REST API v1
Programmatic access to conversations, messages, contacts, ratings, and API key management.
Base URL: https://api.berlay.app/api/v1/{orgSlug}
Authentication
Include your API key as a Bearer token on every request:
Authorization: Bearer brly_sk_your_key_here
Keys are prefixed with brly_sk_ and are 48 characters total. The full key is shown only once at creation time — only a SHA-256 hash is stored.
503 when API disabled: If API access is not enabled for your organization, all v1 endpoints return 503 API_DISABLED.
Scopes
Each API key carries a set of scopes. Calling an endpoint without the required scope returns 403 INSUFFICIENT_SCOPE.
| 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
List
{
"data": [ ... ],
"total": 142,
"limit": 50,
"offset": 0
}Errors
All errors follow a consistent envelope:
{
"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 |
| 404 | NOT_FOUND | Resource does not exist or belongs to a different org |
| 422 | VALIDATION | Request body failed validation |
| 500 | INTERNAL | Unexpected server error |
| 503 | API_DISABLED | API feature is not enabled for this organization |
Conversations
List conversations
GET/api/v1/{orgSlug}/conversationsconversations: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. Default: 50, max: 200. |
| offset | integer | Pagination offset. Default: 0. |
GET /api/v1/acme/conversations?status=open&limit=20
Authorization: Bearer brly_sk_...
{
"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
GET/api/v1/{orgSlug}/conversations/{conversationId}conversations:read
{
"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
POST/api/v1/{orgSlug}/conversationsconversations: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. |
POST /api/v1/acme/conversations
Authorization: Bearer brly_sk_...
Content-Type: application/json
{
"subject": "Billing inquiry",
"contactId": "cont_01j9xyz"
}{
"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
PATCH/api/v1/{orgSlug}/conversations/{conversationId}conversations:write
| Field | Type | Required | Description |
|---|
| subject | string | No | New subject. |
| assignedTo | string | null | No | Agent ID to assign, or null to unassign. |
PATCH /api/v1/acme/conversations/conv_01j9abc
Authorization: Bearer brly_sk_...
Content-Type: application/json
{
"assignedTo": "agent_01j9ghi"
}{
"data": {
"id": "conv_01j9abc",
"subject": "Help with my order",
"status": "open",
"assignedTo": "agent_01j9ghi",
"updatedAt": "2024-01-15T11:05:00.000Z"
}
}Resolve conversation
POST/api/v1/{orgSlug}/conversations/{conversationId}/resolveconversations:write
Marks the conversation as resolved. A system event is recorded.
{
"data": {
"id": "conv_01j9abc",
"status": "resolved",
"updatedAt": "2024-01-15T12:00:00.000Z"
}
}Close conversation
POST/api/v1/{orgSlug}/conversations/{conversationId}/closeconversations:write
Marks the conversation as closed. Closed conversations are archived.
{
"data": {
"id": "conv_01j9abc",
"status": "closed",
"updatedAt": "2024-01-15T12:01:00.000Z"
}
}Reopen conversation
POST/api/v1/{orgSlug}/conversations/{conversationId}/reopenconversations:write
Moves a resolved or closed conversation back to open.
{
"data": {
"id": "conv_01j9abc",
"status": "open",
"updatedAt": "2024-01-15T12:05:00.000Z"
}
}Messages
List messages
GET/api/v1/{orgSlug}/conversations/{conversationId}/messagesmessages:read
| Parameter | Type | Description |
|---|
| limit | integer | Max results. Default: 50, max: 200. |
| offset | integer | Pagination offset. Default: 0. |
{
"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
POST/api/v1/{orgSlug}/conversations/{conversationId}/messagesmessages: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. |
POST /api/v1/acme/conversations/conv_01j9abc/messages
Authorization: Bearer brly_sk_...
Content-Type: application/json
{
"content": "Please find your invoice attached.",
"attachment": {
"url": "https://files.example.com/invoice-1234.pdf",
"filename": "invoice-1234.pdf",
"mimeType": "application/pdf"
}
}{
"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"
}
}
}List contacts
GET/api/v1/{orgSlug}/contactscontacts: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. |
GET /api/v1/acme/contacts?search=jane
Authorization: Bearer brly_sk_...
{
"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
POST/api/v1/{orgSlug}/contactscontacts: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. |
POST /api/v1/acme/contacts
Authorization: Bearer brly_sk_...
Content-Type: application/json
{
"externalId": "user_99",
"name": "Bob Jones",
"email": "bob@example.com"
}{
"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
GET/api/v1/{orgSlug}/contacts/{contactId}contacts:read
{
"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
PATCH/api/v1/{orgSlug}/contacts/{contactId}contacts:write
| Field | Type | Required | Description |
|---|
| name | string | No | New display name. |
| email | string | No | New email. Must be unique per org if provided. |
PATCH /api/v1/acme/contacts/cont_01j9xyz
Authorization: Bearer brly_sk_...
Content-Type: application/json
{
"name": "Jane Smith-Johnson"
}{
"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
POST/api/v1/{orgSlug}/conversations/{conversationId}/rateratings: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. |
POST /api/v1/acme/conversations/conv_01j9abc/rate
Authorization: Bearer brly_sk_...
Content-Type: application/json
{
"rating": 5,
"comment": "Very helpful, resolved quickly!",
"ratedBy": "jane@example.com"
}{
"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 Admin UI sessions. API keys cannot manage other API keys.
List API keys
GET/api/v1/{orgSlug}/api-keysapi_keys:read
{
"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
POST/api/v1/{orgSlug}/api-keysapi_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. |
POST /api/v1/acme/api-keys
Authorization: Bearer brly_sk_...
Content-Type: application/json
{
"name": "CRM sync",
"scopes": ["conversations:read", "contacts:read", "contacts:write"]
}{
"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
DELETE/api/v1/{orgSlug}/api-keys/{keyId}api_keys:write
Immediately invalidates the key. Any subsequent requests using the revoked key receive 401 UNAUTHORIZED.
{
"data": { "revoked": true }
}