# Berlay Complete API Documentation > Source: https://docs.berlay.io | Generated for LLM consumption --- # Quick Start > **Base URL:** `https://api.berlay.app` Berlay provides two ways to integrate with external systems: 1. **Custom Webhooks** — push events into Berlay from your app using a per-channel API key 2. **REST API v1** — read and write conversations, messages, contacts, and ratings programmatically --- ## Overview Each Berlay workspace has an independent data model. All API access is scoped to a single workspace identified by its `orgSlug`. Conversations, contacts, and messages live within that workspace and are not shared across workspaces. --- ## Authentication ### Custom webhook API key Obtained from **Admin → Integrations → Custom / API → Connect**. Pass it as a request header: ```http POST /api/webhooks/custom/{channelId} Content-Type: application/json x-berlay-api-key: brly_your_api_key_here ``` API keys are stored encrypted. If a key is compromised, regenerate it from the integration manage page — the previous key is immediately invalidated. ### REST API Bearer token Created under **Admin → API** in your organization settings. Pass it as an Authorization header: ```http Authorization: Bearer brly_sk_your_key_here ``` API keys are prefixed with `brly_sk_` and are 48 characters long. The full key is shown **only once** at creation time — only a SHA-256 hash is stored. Store keys in a secrets manager, not in source code. --- ## Your first request **Step 1:** Create a Custom / API integration Go to **Admin → Integrations → Custom / API → Connect**, enter a name, and click Connect. **Step 2:** Copy your credentials After connecting you will see your API key (shown once — copy it immediately) and your inbound webhook URL. **Step 3:** Send a test event ```http POST https://api.berlay.app/api/webhooks/custom/123 Content-Type: application/json x-berlay-api-key: brly_your_api_key_here { "messageId": "msg_001", "conversationId": "session_abc", "from": { "externalId": "user_42", "name": "Jane Smith", "email": "jane@example.com" }, "subject": "Help with my order", "content": "Hi, I placed an order but haven't received a confirmation." } ``` The conversation appears in your Berlay inbox within seconds. The endpoint always returns `200 OK` — check the channel event log for delivery status. --- ## Rate limits | Endpoint | Limit | |----------|-------| | `POST /api/auth/magic-link` | 5 requests / minute per IP | | All other endpoints | 60 requests / second per IP | Exceeding the rate limit returns `429 Too Many Requests`. --- ## Next steps - [Custom Webhooks](/webhooks) — full inbound/outbound webhook reference - [REST API v1](/api) — complete endpoint reference --- # Custom Webhook Integration Berlay's custom integration lets you push events from any external system into your workspace using a secure API key. No OAuth flow required — just a single HTTP endpoint and a header. --- ## Setup 1. Go to **Admin → Integrations → Custom / API → Connect** 2. Enter a name for the integration (e.g. "My App Webhook") 3. Optionally enter an **outbound webhook URL** — where Berlay should POST agent replies back to your app 4. Click **Connect** — your API key is shown **once**. Copy it immediately. Your integration is live. Use the **inbound webhook URL** and **API key** shown on the connect screen. --- ## Inbound webhook Send a `POST` request to push messages into Berlay. Each unique `conversationId` becomes a separate conversation thread. Reusing a `conversationId` adds to an existing thread. ### Endpoint ``` POST https://api.berlay.app/api/webhooks/custom/{channelId} ``` ### Headers | Header | Required | Value | |--------|----------|-------| | `Content-Type` | Yes | `application/json` | | `x-berlay-api-key` | Yes | Your channel API key (`brly_...`) | ### Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `messageId` | string | Yes | Unique ID for this message. Used for deduplication. | | `conversationId` | string | Yes | Groups messages into a thread. Reuse to add messages to an existing conversation. | | `from.externalId` | string | Yes | Stable identifier for the sender in your system. | | `from.name` | string | No | Display name of the sender. | | `from.email` | string | No | Email address — used to match or create a contact. | | `content` | string | Yes | Message body text. | | `contentType` | string | No | `text` (default) or `html`. | | `subject` | string | No | Conversation subject. Only used on the first message of a thread. | | `sentAt` | string | No | ISO-8601 timestamp. Defaults to time of receipt. | ### Example request ```http POST https://api.berlay.app/api/webhooks/custom/123 Content-Type: application/json x-berlay-api-key: brly_a1b2c3d4... { "messageId": "msg_01j9xk", "conversationId": "session_abc123", "from": { "externalId": "user_42", "name": "Jane Smith", "email": "jane@example.com" }, "subject": "Need help with my order", "content": "Hi, I placed an order yesterday but haven't received a confirmation email." } ``` ### Response ``` HTTP/1.1 200 OK "ok" ``` The endpoint always returns `200 OK` — even on validation failures — to prevent webhook retry storms. Check the **channel event log** (Admin → Integrations → [your integration] → Recent Events) for per-event delivery status. --- ## Outbound webhook (agent replies) When an agent replies to a conversation from a custom integration, Berlay sends a `POST` request to your configured outbound webhook URL (if set). ### Payload ```json { "conversationId": "session_abc123", "content": "Hello Jane! I can see your order — a confirmation email is on its way." } ``` If no outbound webhook URL is configured, agent replies are stored in Berlay but not forwarded. --- ## API key security - Keys are stored as **SHA-256 hashes** — Berlay never stores plaintext - The plaintext key is shown **once**: at creation time and after regeneration - To rotate: **Admin → Integrations → [your integration] → Regenerate** - Regeneration **immediately invalidates** the previous key - All transport is over HTTPS --- ## Event log Every inbound request is logged with: | Field | Values | |-------|--------| | `status` | `ok` \| `signature_failure` \| `skip` \| `routing_error` | | `eventType` | Event classification | | `error` | Error message if status is not `ok` | | `createdAt` | Timestamp | Access logs at **Admin → Integrations → [your integration] → Recent Events**. --- ## Security ### Outbound webhook validation Berlay validates the outbound webhook URL before storing or sending to it: - **HTTPS required** — `http://` URLs are rejected at configuration time with a `422` error - **Private IP block** — The URL's hostname is resolved via DNS and checked against all RFC private ranges: loopback (127.x, ::1), RFC 1918 (10.x, 172.16–31.x, 192.168.x), link-local (169.254.x — AWS/GCP metadata), CGNAT (100.64–127.x), and IPv6 equivalents including IPv4-mapped addresses - **No redirects** — Berlay sends with `redirect: error`; a redirect to an internal address is rejected - **10 second timeout** — Outbound requests are abandoned after 10 seconds If a URL fails validation, the reply is silently dropped and a warning is logged. You can see blocked deliveries in the channel event log. ### Inbound request security - Every inbound request is verified against your channel's API key using string equality on the `x-berlay-api-key` header - Failed verification is logged as `signature_failure` in the event log - The endpoint always returns `200 OK` regardless of verification outcome — check the event log to diagnose delivery issues ### API key storage - API keys are stored **encrypted at rest** using a per-workspace data encryption key (DEK) wrapped with a master key - The plaintext key is accessible on the manage page and is rotatable at any time - Regenerating a key immediately invalidates the previous one --- # REST API v1 > **Base URL:** `https://api.berlay.app/api/v1/{orgSlug}` The Berlay REST API provides programmatic access to conversations, messages, contacts, ratings, and API key management within a workspace. All endpoints require a Bearer token and are scoped to a single organization. --- ## Authentication Include your API key as a Bearer token on every request: ```http Authorization: Bearer brly_sk_your_key_here ``` **Key format:** Keys are prefixed with `brly_sk_` and are 48 characters total. They are generated with cryptographically secure randomness. **Storage:** Only a SHA-256 hash of the key is stored. The plaintext is shown once at creation time. If lost, revoke and create a new key. **503 when API disabled:** If the API feature is disabled for your organization, all v1 endpoints return: ```json { "error": { "code": "API_DISABLED", "message": "API access is not enabled for this organization." } } ``` --- ## Scopes Each API key is created with a set of scopes controlling which endpoints it may call. | Scope | Description | |-------|-------------| | `conversations:read` | Read conversations and their metadata | | `conversations:write` | Create and update conversations | | `messages:read` | Read messages within conversations | | `messages:write` | Send messages to conversations | | `contacts:read` | Read contact records | | `contacts:write` | Create and update contacts | | `ratings:write` | Submit conversation ratings | | `api_keys:read` | List API keys (session auth only) | | `api_keys:write` | Create and revoke API keys (session auth only) | --- ## Response format ### Single resource ```json { "data": { ... } } ``` ### List ```json { "data": [ ... ], "total": 142, "limit": 50, "offset": 0 } ``` --- ## Errors All errors follow a consistent shape: ```json { "error": { "code": "ERROR_CODE", "message": "Human-readable description." } } ``` | HTTP Status | Code | When | |-------------|------|------| | 401 | `UNAUTHORIZED` | Missing or invalid Bearer token | | 403 | `INSUFFICIENT_SCOPE` | Token lacks the required scope for this endpoint | | 404 | `NOT_FOUND` | Resource does not exist or belongs to a different org | | 422 | `VALIDATION` | Request body failed validation; `message` describes the field | | 500 | `INTERNAL` | Unexpected server error | | 503 | `API_DISABLED` | API feature is not enabled for this organization | --- ## Conversations ### List conversations ```http GET /api/v1/{orgSlug}/conversations Authorization: Bearer brly_sk_... ``` **Required scope:** `conversations:read` | Parameter | Type | Description | |-----------|------|-------------| | `status` | string | Filter by status: `open`, `resolved`, `closed`. Omit for all. | | `assignedTo` | string | Filter by agent ID. | | `limit` | integer | Max results to return. Default: `50`, max: `200`. | | `offset` | integer | Pagination offset. Default: `0`. | **Example request:** ```http GET /api/v1/acme/conversations?status=open&limit=20 Authorization: Bearer brly_sk_... ``` **Example response:** ```json { "data": [ { "id": "conv_01j9abc", "subject": "Help with my order", "status": "open", "assignedTo": null, "contactId": "cont_01j9xyz", "channelType": "custom", "createdAt": "2024-01-15T10:30:00.000Z", "updatedAt": "2024-01-15T10:35:00.000Z" } ], "total": 84, "limit": 20, "offset": 0 } ``` --- ### Get conversation ```http GET /api/v1/{orgSlug}/conversations/{conversationId} Authorization: Bearer brly_sk_... ``` **Required scope:** `conversations:read` **Example response:** ```json { "data": { "id": "conv_01j9abc", "subject": "Help with my order", "status": "open", "assignedTo": null, "contactId": "cont_01j9xyz", "channelType": "custom", "externalConversationId": "session_abc123", "createdAt": "2024-01-15T10:30:00.000Z", "updatedAt": "2024-01-15T10:35:00.000Z" } } ``` --- ### Create conversation ```http POST /api/v1/{orgSlug}/conversations Authorization: Bearer brly_sk_... Content-Type: application/json ``` **Required scope:** `conversations:write` | Field | Type | Required | Description | |-------|------|----------|-------------| | `subject` | string | Yes | Conversation subject line. | | `contactId` | string | No | ID of an existing contact to associate. | | `assignedTo` | string | No | Agent ID to assign immediately. | | `channelType` | string | No | Defaults to `api`. | **Example request:** ```json { "subject": "Billing inquiry", "contactId": "cont_01j9xyz" } ``` **Example response:** ```json { "data": { "id": "conv_01j9def", "subject": "Billing inquiry", "status": "open", "assignedTo": null, "contactId": "cont_01j9xyz", "channelType": "api", "createdAt": "2024-01-15T11:00:00.000Z", "updatedAt": "2024-01-15T11:00:00.000Z" } } ``` --- ### Update conversation ```http PATCH /api/v1/{orgSlug}/conversations/{conversationId} Authorization: Bearer brly_sk_... Content-Type: application/json ``` **Required scope:** `conversations:write` | Field | Type | Required | Description | |-------|------|----------|-------------| | `subject` | string | No | New subject. | | `assignedTo` | string | No | Agent ID to assign, or `null` to unassign. | **Example request:** ```json { "assignedTo": "agent_01j9ghi" } ``` **Example response:** ```json { "data": { "id": "conv_01j9abc", "subject": "Help with my order", "status": "open", "assignedTo": "agent_01j9ghi", "updatedAt": "2024-01-15T11:05:00.000Z" } } ``` --- ### Resolve conversation ```http POST /api/v1/{orgSlug}/conversations/{conversationId}/resolve Authorization: Bearer brly_sk_... ``` **Required scope:** `conversations:write` Marks the conversation as `resolved`. A system event is recorded. **Example response:** ```json { "data": { "id": "conv_01j9abc", "status": "resolved", "updatedAt": "2024-01-15T12:00:00.000Z" } } ``` --- ### Close conversation ```http POST /api/v1/{orgSlug}/conversations/{conversationId}/close Authorization: Bearer brly_sk_... ``` **Required scope:** `conversations:write` Marks the conversation as `closed`. Closed conversations are archived. **Example response:** ```json { "data": { "id": "conv_01j9abc", "status": "closed", "updatedAt": "2024-01-15T12:01:00.000Z" } } ``` --- ### Reopen conversation ```http POST /api/v1/{orgSlug}/conversations/{conversationId}/reopen Authorization: Bearer brly_sk_... ``` **Required scope:** `conversations:write` Moves a `resolved` or `closed` conversation back to `open`. **Example response:** ```json { "data": { "id": "conv_01j9abc", "status": "open", "updatedAt": "2024-01-15T12:05:00.000Z" } } ``` --- ## Messages ### List messages ```http GET /api/v1/{orgSlug}/conversations/{conversationId}/messages Authorization: Bearer brly_sk_... ``` **Required scope:** `messages:read` | Parameter | Type | Description | |-----------|------|-------------| | `limit` | integer | Max results. Default: `50`, max: `200`. | | `offset` | integer | Pagination offset. Default: `0`. | **Example response:** ```json { "data": [ { "id": "msg_01j9aaa", "conversationId": "conv_01j9abc", "role": "customer", "content": "Hi, I placed an order but haven't received a confirmation.", "contentType": "text", "sentAt": "2024-01-15T10:30:00.000Z", "attachment": null }, { "id": "msg_01j9bbb", "conversationId": "conv_01j9abc", "role": "agent", "content": "Hello! Let me look into that for you.", "contentType": "text", "sentAt": "2024-01-15T10:32:00.000Z", "attachment": null } ], "total": 2, "limit": 50, "offset": 0 } ``` --- ### Send message ```http POST /api/v1/{orgSlug}/conversations/{conversationId}/messages Authorization: Bearer brly_sk_... Content-Type: application/json ``` **Required scope:** `messages:write` | Field | Type | Required | Description | |-------|------|----------|-------------| | `content` | string | Yes | Message body. | | `contentType` | string | No | `text` (default) or `html`. | | `attachment.url` | string | No | Publicly accessible URL to the attached file. | | `attachment.filename` | string | No | Original filename shown to the recipient. | | `attachment.mimeType` | string | No | MIME type, e.g. `application/pdf`, `image/png`. | | `author` | object | No | Optional author attribution for API messages. | | `author.name` | string | Yes (if author set) | Display name shown in the inbox. | | `author.email` | string | No | Email of the external staff member. | | `author.externalId` | string | No | Stable ID in your system for this staff member. | **Example request:** ```json { "content": "Please find your invoice attached.", "author": { "name": "Jane Smith", "externalId": "agent_42" }, "attachment": { "url": "https://files.example.com/invoice-1234.pdf", "filename": "invoice-1234.pdf", "mimeType": "application/pdf" } } ``` When `author` is provided, the message is displayed in the Berlay inbox as if sent by a named staff member, even without a Berlay account. The author name appears in place of the generic 'Agent' label. **Example response:** ```json { "data": { "id": "msg_01j9ccc", "conversationId": "conv_01j9abc", "role": "agent", "content": "Please find your invoice attached.", "contentType": "text", "sentAt": "2024-01-15T11:10:00.000Z", "attachment": { "url": "https://files.example.com/invoice-1234.pdf", "filename": "invoice-1234.pdf", "mimeType": "application/pdf" } } } ``` --- ## Contacts ### List contacts ```http GET /api/v1/{orgSlug}/contacts Authorization: Bearer brly_sk_... ``` **Required scope:** `contacts:read` | Parameter | Type | Description | |-----------|------|-------------| | `search` | string | Full-text search across name, email, and externalId. | | `limit` | integer | Max results. Default: `50`, max: `200`. | | `offset` | integer | Pagination offset. Default: `0`. | **Example request:** ```http GET /api/v1/acme/contacts?search=jane Authorization: Bearer brly_sk_... ``` **Example response:** ```json { "data": [ { "id": "cont_01j9xyz", "externalId": "user_42", "name": "Jane Smith", "email": "jane@example.com", "createdAt": "2024-01-10T09:00:00.000Z", "updatedAt": "2024-01-15T10:30:00.000Z" } ], "total": 1, "limit": 50, "offset": 0 } ``` --- ### Create contact ```http POST /api/v1/{orgSlug}/contacts Authorization: Bearer brly_sk_... Content-Type: application/json ``` **Required scope:** `contacts:write` | Field | Type | Required | Description | |-------|------|----------|-------------| | `externalId` | string | No | Stable ID from your system. Must be unique per org. | | `name` | string | No | Display name. | | `email` | string | No | Email address. Must be unique per org if provided. | **Example request:** ```json { "externalId": "user_99", "name": "Bob Jones", "email": "bob@example.com" } ``` **Example response:** ```json { "data": { "id": "cont_01j9qrs", "externalId": "user_99", "name": "Bob Jones", "email": "bob@example.com", "createdAt": "2024-01-15T11:20:00.000Z", "updatedAt": "2024-01-15T11:20:00.000Z" } } ``` --- ### Get contact ```http GET /api/v1/{orgSlug}/contacts/{contactId} Authorization: Bearer brly_sk_... ``` **Required scope:** `contacts:read` **Example response:** ```json { "data": { "id": "cont_01j9xyz", "externalId": "user_42", "name": "Jane Smith", "email": "jane@example.com", "createdAt": "2024-01-10T09:00:00.000Z", "updatedAt": "2024-01-15T10:30:00.000Z" } } ``` --- ### Update contact ```http PATCH /api/v1/{orgSlug}/contacts/{contactId} Authorization: Bearer brly_sk_... Content-Type: application/json ``` **Required scope:** `contacts:write` | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | No | New display name. | | `email` | string | No | New email address. Must be unique per org if provided. | **Example request:** ```json { "name": "Jane Smith-Johnson" } ``` **Example response:** ```json { "data": { "id": "cont_01j9xyz", "externalId": "user_42", "name": "Jane Smith-Johnson", "email": "jane@example.com", "updatedAt": "2024-01-15T13:00:00.000Z" } } ``` --- ## Ratings ### Submit rating ```http POST /api/v1/{orgSlug}/conversations/{conversationId}/rate Authorization: Bearer brly_sk_... Content-Type: application/json ``` **Required scope:** `ratings:write` | Field | Type | Required | Description | |-------|------|----------|-------------| | `rating` | integer | Yes | Score from `1` (worst) to `5` (best). | | `comment` | string | No | Optional free-text feedback. | | `ratedBy` | string | No | Identifier of the person submitting the rating (e.g. customer email or externalId). | **Example request:** ```json { "rating": 5, "comment": "Very helpful, resolved quickly!", "ratedBy": "jane@example.com" } ``` **Example response:** ```json { "data": { "id": "rate_01j9uvw", "conversationId": "conv_01j9abc", "rating": 5, "comment": "Very helpful, resolved quickly!", "ratedBy": "jane@example.com", "createdAt": "2024-01-15T14:00:00.000Z" } } ``` --- ## API Keys > **Session auth only.** The `api_keys:read` and `api_keys:write` scopes are reserved for keys created via the Admin UI and authenticated using a session cookie. API keys cannot manage other API keys. ### List API keys ```http GET /api/v1/{orgSlug}/api-keys Authorization: Bearer brly_sk_... ``` **Required scope:** `api_keys:read` **Example response:** ```json { "data": [ { "id": "key_01j9lmn", "name": "Production integration", "prefix": "brly_sk_a1b2", "scopes": ["conversations:read", "messages:read"], "createdAt": "2024-01-01T00:00:00.000Z", "lastUsedAt": "2024-01-15T10:30:00.000Z" } ], "total": 1, "limit": 50, "offset": 0 } ``` --- ### Create API key ```http POST /api/v1/{orgSlug}/api-keys Authorization: Bearer brly_sk_... Content-Type: application/json ``` **Required scope:** `api_keys:write` | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Human-readable label for this key. | | `scopes` | string[] | Yes | Array of scope strings. Must be a valid subset of available scopes. | **Example request:** ```json { "name": "CRM sync", "scopes": ["conversations:read", "contacts:read", "contacts:write"] } ``` **Example response:** ```json { "data": { "id": "key_01j9opq", "name": "CRM sync", "key": "brly_sk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2", "prefix": "brly_sk_a1b2", "scopes": ["conversations:read", "contacts:read", "contacts:write"], "createdAt": "2024-01-15T15:00:00.000Z" } } ``` > The `key` field contains the full plaintext key. It is **only present in this response** — store it immediately in a secrets manager. Subsequent calls return only the `prefix`. --- ### Revoke API key ```http DELETE /api/v1/{orgSlug}/api-keys/{keyId} Authorization: Bearer brly_sk_... ``` **Required scope:** `api_keys:write` Immediately invalidates the key. Any requests using the revoked key will receive `401 UNAUTHORIZED`. **Example response:** ```json { "data": { "revoked": true } } ```