# Qlara Platform API -- Full Documentation > Unified REST API for multi-channel messaging: SMS, RCS, and WhatsApp. ## Overview The Qlara Platform is a messaging platform API that lets you send messages across SMS, RCS, and WhatsApp channels through a single integration. It provides contacts management, campaign orchestration, delivery tracking, webhook notifications, template management, and media management. --- ## Base URL ``` https://lora-api.agiletelecom.com/api ``` Every endpoint path documented here is relative to this base URL. --- ## Authentication Every request must include valid credentials. The API supports two methods: ### API Key (recommended) Pass your API key in the `X-Api-Key` header: ```bash curl -X GET "https://lora-api.agiletelecom.com/api/partner-gateway/v1/contacts" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Accept: application/json" ``` ### Basic Auth Pass your credentials as a Base64-encoded `username:password` in the `Authorization` header: ```bash curl -X GET "https://lora-api.agiletelecom.com/api/partner-gateway/v1/contacts" \ -H "Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=" \ -H "Accept: application/json" ``` API Key authentication is recommended for all new integrations. Never expose credentials in client-side code, public repositories, or URLs. ### IP Whitelist Your account may have an optional IP allowlist configured in the Qlara platform. When enabled, requests from non-allowlisted IPs are rejected with `403 Forbidden` -- whitelist your server's egress IPs before going live. --- ## Rate Limits The messaging endpoints under `/api/message-server/` apply a per-account limit on requests per second, set by your subscription plan. When you exceed it, the API answers `429 Too Many Requests` with an empty body and a `Retry-After: 1` header: wait one second and retry. The Qlara Platform endpoints under `/api/partner-gateway/v1/` apply no rate limit at the moment and do not answer `429`. No `X-RateLimit-*` headers are returned by either family of endpoints. For delivery status, prefer the webhook over tight polling loops. --- ## Pagination Endpoints that return collections (contacts, contact lists, campaigns, exports, media, RCS agents, WhatsApp phone numbers, message history) are page-based: `page` selects a page, counting from 0, and `limit` sets the page size. | Parameter | Type | Default | Description | |-----------|---------|----------------------------|-------------| | `page` | integer | `0` | Page index. The first page is 0 | | `limit` | integer | `10` or `20`, per endpoint | Page size. Where a maximum is enforced it is `1000` | Example request: ```bash curl -X GET "https://lora-api.agiletelecom.com/api/partner-gateway/v1/contacts?page=2&limit=20" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Accept: application/json" ``` Example response: ```json { "data": [ ... ], "page": 2, "limit": 20, "totalCount": 1250, "totalPages": 63 } ``` Iterate by incrementing `page` from `0` to `totalPages - 1`. A few collections have a different shape: - `GET /partner-gateway/v1/campaigns` wraps the same page object in a JSend envelope: `{ "status": "success", "data": { "data": [...], "page": 0, "limit": 20, "totalCount": 12, "totalPages": 1 } }`. - `GET /partner-gateway/v1/messages/history` and `GET /partner-gateway/v1/messages/status` return a plain JSON array. History still accepts `page` and `limit`: you have reached the last page when it comes back with fewer items than `limit`. - `GET /partner-gateway/v1/inbox/conversations` and `GET /partner-gateway/v1/socials` return a plain JSON array with no pagination. --- ## Error Handling ### Error response format The API uses standard HTTP status codes. The Qlara Platform endpoints return a JSend body. A request that was refused (`4xx`) is a `fail`, and `data` says why. It is a string in most cases, and a list of messages when request-body validation fails: ```json { "status": "fail", "data": "Invalid 'from': '27/08/2026'. Accepted formats: yyyy-MM-dd (a whole day, UTC), yyyy-MM-ddTHH:mm:ss (UTC) or ISO-8601 with an offset such as 2025-01-15T00:00:00+01:00 or 2025-01-15T00:00:00Z" } ``` A failure on our side (`5xx`) is an `error`: ```json { "status": "error", "message": "Something bad happened. Please try again!" } ``` Two exceptions to this shape: a missing or invalid API key answers `401` with `{"error": "Invalid API Key"}`, and a `429` from the messaging endpoints has no body. ### HTTP Status Codes | Code | Meaning | Description | |-------|------------------------|-----------------------------------------------------------------------------| | `400` | Bad Request | Invalid request body or query parameters. `data` names the parameter and, for dates and enumerations, the accepted values. | | `401` | Unauthorized | Missing or invalid `X-Api-Key`, or a key that does not include the operation you called. | | `403` | Forbidden | The request is not allowed for your account, for example a sender ID your company does not own. | | `404` | Not Found | The endpoint or the resource does not exist for your account. A status lookup answers `404` when the message id is unknown for the given channel. | | `409` | Conflict | Resource already exists (e.g. webhook already configured). | | `422` | Unprocessable Entity | Send endpoints (`POST /partner-gateway/v1/sms/messages`, `/rcs/messages`, `/whatsapp/messages`): the messaging platform refused the message. JSend `fail` body. Not retryable as-is. If a fallback channel accepted the message, the answer is still `202`. | | `429` | Too Many Requests | Messaging endpoints (`/api/message-server/`) only: the per-account requests-per-second limit was exceeded. Honour `Retry-After`. | | `500` | Internal Server Error | Unexpected server error. Retry with exponential backoff and quote the `X-Request-Id` to support if it persists. | | `502` | Bad Gateway | An upstream channel provider returned an error. | ### Retry Strategy For transient errors (`429` and `5xx`), implement exponential backoff with jitter: 1. First retry: wait 1 second 2. Second retry: wait 2 seconds 3. Third retry: wait 4 seconds 4. Maximum retries: 5 attempts 5. Add jitter: random delay of 0-500ms to each wait time Do NOT retry `400`, `401`, `403`, `404` or `422` errors. --- ## Response Format All responses use JSON with UTF-8 encoding. Common headers: - `Content-Type: application/json;charset=UTF-8` - `X-Request-Id: 16d9b2a9ca97da0d8c4c4ea2ae689017` -- the trace identifier of the request (32 hex characters, not a UUID), the same key that indexes every log line the request produced. Present on every response, errors included (`401`, `500`). Log it and quote it to support - `Cache-Control: no-cache, no-store, must-revalidate` Successful responses: - Single resource: object at top level - Collections: the page object described under Pagination, or a plain array for the endpoints listed there - Create: `201 Created` with the created resource - Update: `200 OK` with the updated resource - Delete: `204 No Content` for most resources. `DELETE /partner-gateway/v1/contacts` and `DELETE /partner-gateway/v1/contacts/list` answer `200 OK` with `true`, and take the ids to delete in the `ids` query parameter, comma-separated, not in the body - Async operations (exports): `202 Accepted` with an empty body. Poll `GET /partner-gateway/v1/exports` until the export shows `isAvailableForDownload: true`, then call `GET /partner-gateway/v1/exports/{exportId}` for the download URL Timestamps come in two formats, depending on the resource: | Where | Format | Example | |-------|--------|---------| | Delivery status and message history (`sendDate`, `deliveryDate`, `readDate`) | ISO 8601 with offset | `2026-09-03T08:28:52Z` | | Contacts, lists, campaigns, exports, inbox (`createdAt`, `lastMessageDate`, ...) and `scheduledDate` on sends | `yyyy-MM-dd HH:mm:ss.SSSZ` | `2026-09-01 10:17:53.200+0000` | --- ## Versioning Qlara Platform endpoints: `/api/partner-gateway/v1/...` Channel-specific endpoints use flat paths: `/api/message-server/sms/send`, `/api/message-server/rcs/send`, `/api/message-server/whatsapp/send` Breaking changes get a new version (`v2`) with at least 6 months deprecation overlap. --- # Channel APIs -- Sending Messages --- ## SMS Universal API Modern format, one message per request, supports placeholders. ### Endpoint `POST /message-server/sms/send` ### Request Body | Field | Type | Required | Description | |--------------------|---------|----------|-------------| | `destination` | string | Yes | Recipient number in international format (e.g. `+393201234567`) | | `sender` | string | Yes | Alphanumeric (max 11 chars) or numeric sender | | `body` | string | Yes | Message text. Supports `{name}` placeholder syntax | | `campaignId` | string | No | Campaign ID for grouping and reporting (max 255 chars) | | `messageId` | string | No | Custom message ID. Auto-generated UUID if omitted | | `udhData` | string | No | UDH data in hexadecimal format for binary SMS | | `simulation` | boolean | No | If `true`, validates without sending. Default: `false` | | `enableNotification` | boolean | No | If `true`, enables delivery callbacks. Default: `true` | | `placeholders` | object | No | Key-value map for placeholder substitution. E.g. `{"nome": "Marco"}` replaces `{nome}` in the body | | `scheduledDate` | string | No | Scheduled date in `yyyy-MM-dd HH:mm:ss.SSSZ` format | | `skipRcsOverride` | boolean | No | If `true`, disables RCS override. Default: `false` | ### Response ```json { "messageId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "simulation": false, "results": { "sms": { "accepted": true, "unicode": false, "parts": 1, "reasons": [] } } } ``` | Field | Type | Description | |------------------------|---------|-------------| | `messageId` | string | UUID of the sent message | | `simulation` | boolean | Whether it was a dry run | | `results.sms.accepted` | boolean | Whether accepted for sending | | `results.sms.unicode` | boolean | Whether Unicode encoding was used | | `results.sms.parts` | integer | Number of SMS parts (concatenation) | | `results.sms.reasons` | array | Rejection/warning reasons | ### Curl Examples Simple SMS: ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/sms/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393201234567", "sender": "MyBrand", "body": "Ciao, il tuo ordine e stato spedito!" }' ``` SMS with placeholders: ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/sms/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393201234567", "sender": "MyBrand", "body": "Ciao {nome}, il tuo codice e {codice}.", "placeholders": {"nome": "Marco", "codice": "ABC123"}, "campaignId": "promo-2024-01", "enableNotification": true }' ``` Scheduled SMS: ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/sms/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393201234567", "sender": "MyBrand", "body": "Promemoria: appuntamento domani alle 10:00.", "scheduledDate": "2024-12-20 09:00:00.000+0100" }' ``` --- ## SMS Legacy API Backward-compatible format, multi-recipient per request. ### Send Endpoint `POST /services/sms/send` ### Request Body | Field | Type | Required | Description | |---------------------|---------|----------|-------------| | `globalId` | string | No | Global ID of the sending session. Auto-generated if omitted | | `enableConcatenated`| boolean | No | Enable concatenation for long messages. Default: `true` | | `enableUnicode` | boolean | No | Enable Unicode encoding. Default: `true` | | `enableDelivery` | boolean | No | Enable delivery notifications. Default: `true` | | `simulation` | boolean | No | Process but do not send. Default: `false` | | `skipRcsOverride` | boolean | No | Disable automatic RCS override. Default: `false` | | `messages` | array | Yes | Array of message objects (see below) | Message object: | Field | Type | Required | Description | |----------------|--------|----------|-------------| | `destinations` | array | Yes | List of recipient numbers in international format | | `ids` | array | No | Custom IDs, one per recipient. Must match `destinations` length | | `sender` | string | Yes | SMS sender (alphanumeric max 11 chars or numeric) | | `body` | string | Yes | SMS message text | | `hexBody` | boolean| No | If true, `body` is in hexadecimal format. Default: `false` | | `udhData` | string | No | UDH in hexadecimal format | | `scheduling` | string | No | Scheduled date/time: `yyyy-MM-dd HH:mm:ss.SSSZ` | ### Response ```json { "globalId": "campaign-2025-001", "processedMessages": 1, "processedSmsParts": 2, "credit": 150.50 } ``` | Field | Type | Description | |--------------------|---------|-------------| | `globalId` | string | Session ID | | `processedMessages`| integer | Number of processed messages | | `processedSmsParts`| integer | Total SMS parts processed | | `credit` | number | Remaining credit after sending | ### Check Credit `GET /services/sms/credit` Response: `{ "credit": 150.50 }` ### Curl Example ```bash curl -X POST "https://lora-api.agiletelecom.com/api/services/sms/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enableConcatenated": true, "enableUnicode": true, "enableDelivery": true, "messages": [ { "destinations": ["+393401234567", "+393407654321"], "sender": "MyCompany", "body": "Promo: 20% di sconto su tutti i prodotti!" } ] }' ``` --- ## RCS Send API Rich messages: text, cards, carousels with buttons and media. Supports fallback to WhatsApp and SMS. ### Endpoint `POST /message-server/rcs/send` ### Request Body | Field | Type | Required | Description | |--------------------|---------|----------|-------------| | `destination` | string | Yes | Recipient number in international format | | `agentId` | integer | Yes | Sender RCS agent ID | | `body` | object | No* | Inline message body (mutually exclusive with `templateId`) | | `body.type` | string | Yes** | `TEXT`, `CARD`, or `CAROUSEL` | | `body.body` | object | Yes** | Content (varies by type) | | `templateId` | integer | No* | RCS template ID (mutually exclusive with `body`) | | `campaignId` | string | No | Campaign ID for grouping (max 255 chars) | | `messageId` | string | No | Custom message ID. Auto-generated UUID if omitted | | `simulation` | boolean | No | Dry run mode. Default: `false` | | `enableNotification`| boolean| No | Enable delivery/read notifications. Default: `true` | | `maxSmsParts` | integer | No | Max SMS parts the SMS fallback may occupy. If not set, no SMS fallback; if the fallback text exceeds this limit, the SMS fallback is skipped (not truncated) | | `fallbackWhatsApp` | object | No | RCS -> WhatsApp fallback. If RCS fails, the message is attempted via WhatsApp before SMS. If present, `fallbackSms` is mandatory | | `fallbackWhatsApp.phoneNumberId` | integer | Yes*** | WhatsApp Business sender phone number ID | | `fallbackWhatsApp.templateId` | integer | No | WhatsApp template ID (mutually exclusive with `fallbackWhatsApp.body`) | | `fallbackWhatsApp.body` | object | No | Inline WhatsApp body (mutually exclusive with `fallbackWhatsApp.templateId`) | | `fallbackSms` | object | No | RCS -> SMS fallback. Mandatory when `fallbackWhatsApp` is present | | `fallbackSms.sender` | string | Yes**** | SMS sender (alphanumeric or numeric) | | `fallbackSms.text` | string | Yes**** | Fallback SMS text | | `placeholders` | object | No | Key-value map for placeholder substitution | | `scheduledDate` | string | No | Scheduled date: `yyyy-MM-dd HH:mm:ss.SSSZ` | *One of `body` or `templateId` is required. **Required when using `body`. ***Required when using `fallbackWhatsApp`. ****Required when using `fallbackSms`. #### Body Types TEXT body: ```json { "type": "TEXT", "body": { "text": "Ciao! Il tuo ordine e stato confermato." } } ``` CARD body: ```json { "type": "CARD", "body": { "title": "Offerta speciale", "description": "Scopri la nostra promozione esclusiva", "mediaUrl": "https://example.com/promo.jpg", "suggestions": [ {"type": "URL", "text": "Scopri di piu", "value": "https://example.com/promo"} ] } } ``` CAROUSEL body: ```json { "type": "CAROUSEL", "body": { "cards": [ { "title": "Prodotto 1", "description": "Descrizione prodotto 1", "mediaUrl": "https://example.com/prod1.jpg", "suggestions": [ {"type": "URL", "text": "Dettagli", "value": "https://example.com/prod1"} ] }, { "title": "Prodotto 2", "description": "Descrizione prodotto 2", "mediaUrl": "https://example.com/prod2.jpg", "suggestions": [ {"type": "URL", "text": "Dettagli", "value": "https://example.com/prod2"} ] } ] } } ``` ### Response ```json { "messageId": "e76614d1-4ac1-4d94-89f0-d07f1b5a190c", "simulation": false, "results": { "rcs": { "accepted": true, "reasons": [] }, "sms": null } } ``` If RCS fails and SMS fallback is active, `results.sms` will contain the SMS result. ### Curl Examples Text message: ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/rcs/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393401234567", "agentId": 10, "body": { "type": "TEXT", "body": {"text": "Ciao! Il tuo ordine e stato confermato."} }, "enableNotification": true }' ``` Send with template: ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/rcs/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393401234567", "agentId": 10, "templateId": 42, "placeholders": {"name": "Mario", "order": "ORD-12345"}, "enableNotification": true }' ``` Send with SMS fallback: ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/rcs/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393401234567", "agentId": 10, "body": { "type": "TEXT", "body": {"text": "Ciao! Il tuo ordine e stato confermato."} }, "maxSmsParts": 3, "enableNotification": true }' ``` Send with WhatsApp + SMS fallback chain (RCS -> WhatsApp -> SMS): ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/rcs/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393401234567", "agentId": 10, "templateId": 42, "placeholders": {"firstName": "Mario"}, "fallbackWhatsApp": {"phoneNumberId": 5, "templateId": 99}, "fallbackSms": {"sender": "MyCompany", "text": "Hi Mario, your order has been shipped."} }' ``` > If `fallbackWhatsApp` is present, `fallbackSms` is mandatory. For a simple RCS -> SMS chain, supply only `fallbackSms` (and optionally `maxSmsParts`). --- ## WhatsApp API Templates (Meta-approved) and free-form messages within 24h window. Content types: text, image, video, audio, document, location, sticker, reaction. Supports fallback chain: WhatsApp -> RCS -> SMS. ### Phone Numbers `GET /message-server/whatsapp/phone-numbers` -- List WhatsApp Business numbers `GET /message-server/whatsapp/phone-numbers/{id}` -- Get number details Response: ```json { "data": [ { "id": 5, "phoneNumber": "+393209998877", "displayName": "My Business", "onboardingScope": "RECEIVE_AND_SEND" } ] } ``` Use the `id` field as `phoneNumberId` in send requests. ### Send Endpoint `POST /message-server/whatsapp/send` ### Request Body | Field | Type | Required | Description | |--------------------|---------|----------|-------------| | `destination` | string | Yes | Recipient phone number in international format | | `phoneNumberId` | integer | Yes | Sender WhatsApp Business phone number ID | | `template` | object | No* | Template reference (mutually exclusive with `body`) | | `template.id` | integer | Yes** | Template ID | | `template.mediaUrl`| string | No | Media URL for template header | | `body` | object | No* | Free-form content (only within 24h window). Must contain exactly one sub-object | | `campaignId` | string | No | Campaign ID (max 255 chars) | | `messageId` | string | No | Custom message ID. Auto-generated if omitted | | `simulation` | boolean | No | Dry run. Default: `false` | | `enableNotification`| boolean| No | Enable delivery/read notifications. Default: `true` | | `placeholders` | object | No | Key-value map for placeholders. Also used for tracked links: `{"shortLinkT1": "https://..."}` | | `scheduledDate` | string | No | Scheduled date: `yyyy-MM-dd HH:mm:ss.SSSZ` | | `fallbackRcs` | object | No | RCS fallback config. If present, `fallbackSms` is required | | `fallbackRcs.agentId`| integer| Yes*** | RCS agent ID | | `fallbackRcs.templateId`| integer | No | RCS template ID (mutually exclusive with `fallbackRcs.body`) | | `fallbackRcs.type` | string | No | RCS body type: `TEXT`, `CARD`, `CAROUSEL` (required when using `fallbackRcs.body`) | | `fallbackRcs.body` | object | No | Inline RCS body (mutually exclusive with `fallbackRcs.templateId`) | | `fallbackSms` | object | No | SMS fallback config | | `fallbackSms.sender`| string | Yes**** | SMS sender | | `fallbackSms.text` | string | Yes**** | Fallback SMS text | *One of `template` or `body` is required. **Required when using `template`. ***Required when using `fallbackRcs`. ****Required when using `fallbackSms`. > `fallbackSms` may be supplied without `fallbackRcs` for a direct WhatsApp -> SMS fallback. If `fallbackRcs` is present, `fallbackSms` is required. #### Free-form Body Types The `body` object must contain exactly one of: - `text` -- `{ "body": "Message text" }` - `image` -- `{ "url": "https://...", "caption": "..." }` (or `key` instead of `url`) - `video` -- `{ "url": "https://...", "caption": "..." }` - `audio` -- `{ "url": "https://..." }` - `document` -- `{ "url": "https://...", "filename": "file.pdf", "caption": "..." }` - `sticker` -- `{ "url": "https://..." }` - `location` -- `{ "latitude": "45.4642", "longitude": "9.1900", "name": "Store", "address": "Via Roma 1" }` - `reaction` -- `{ "messageId": "msg-id", "emoji": "thumbs-up" }` - `replyMessageId` -- string, ID of message to reply to (usable with any type) ### Response ```json { "messageId": "e76614d1-4ac1-4d94-89f0-d07f1b5a190c", "simulation": false, "results": { "whatsapp": {"accepted": true}, "rcs": null, "sms": null } } ``` ### Download Media from Inbound Messages `GET /files/{mediaKey}?expireMinutes=180` Returns a pre-signed temporary URL to download a media file received in an inbound message. ### Curl Examples Send with template: ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/whatsapp/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393401234567", "phoneNumberId": 5, "template": {"id": 42}, "enableNotification": true }' ``` Template + placeholder + media header: ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/whatsapp/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393401234567", "phoneNumberId": 5, "template": {"id": 42, "mediaUrl": "https://example.com/header-image.jpg"}, "placeholders": {"name": "Mario", "order": "ORD-12345"}, "enableNotification": true }' ``` Free-form text (within 24h window): ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/whatsapp/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393401234567", "phoneNumberId": 5, "body": {"text": {"body": "Ciao! Il tuo ordine e stato spedito."}} }' ``` Send image: ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/whatsapp/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393401234567", "phoneNumberId": 5, "body": {"image": {"url": "https://example.com/product.jpg", "caption": "Ecco il tuo prodotto!"}} }' ``` Send document: ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/whatsapp/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393401234567", "phoneNumberId": 5, "body": {"document": {"url": "https://example.com/invoice.pdf", "filename": "fattura_marzo.pdf", "caption": "La tua fattura"}} }' ``` Send location: ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/whatsapp/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393401234567", "phoneNumberId": 5, "body": {"location": {"latitude": "45.4642", "longitude": "9.1900", "name": "Negozio Milano Centro", "address": "Via Roma 1, 20121 Milano"}} }' ``` Send with fallback chain (WhatsApp -> RCS -> SMS): ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/whatsapp/send" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "+393401234567", "phoneNumberId": 5, "template": {"id": 42}, "fallbackRcs": {"agentId": 10, "templateId": 99}, "fallbackSms": {"sender": "MyCompany", "text": "Ciao! Visita https://example.com"}, "placeholders": {"name": "Mario"} }' ``` --- # Template Management --- ## RCS Templates Pre-configured messages for RCS. Three formats: TEXT, CARD, CAROUSEL. ### Endpoints | Method | Path | Description | |----------|-------------------------------------------|-------------------| | `GET` | `/message-server/rcs/templates` | List templates | | `GET` | `/message-server/rcs/templates/{id}` | Template details | | `POST` | `/message-server/rcs/templates` | Create template | | `PUT` | `/message-server/rcs/templates/{id}` | Update template | | `DELETE` | `/message-server/rcs/templates/{id}` | Delete template | ### Template Creation Fields | Field | Type | Required | Description | |---------------|--------|----------|-------------| | `name` | string | Yes | Template name | | `description` | string | No | Description | | `type` | string | Yes | `TEXT`, `CARD`, or `CAROUSEL` | | `body` | object | Yes | Content (structure varies by type) | ### TEXT Template Body | Field | Type | Required | Description | |------------------|--------|----------|-------------| | `text` | string | Yes | Message text. Supports `{variableName}` placeholders | | `suggestions` | array | No | Interactive suggestions (buttons) | | `ttlSeconds` | integer| No | Time-to-live in seconds before fallback | | `fallbackSms` | object | No | SMS fallback: `{ "sender": "...", "text": "..." }` | | `fallbackWhatsApp`| object| No | WhatsApp fallback | ### CARD Template Body | Field | Type | Required | Description | |---------------------|--------|----------|-------------| | `cardOrientation` | string | Yes | `HORIZONTAL`, `VERTICAL`, or `UNSPECIFIED` | | `thumbnailAlignment`| string | No | `LEFT`, `RIGHT`, or `UNSPECIFIED` | | `card` | object | Yes | Card with title, description, media, suggestions | | `card.title` | string | No | Card title | | `card.description` | string | No | Card description | | `card.media` | object | Yes | Media config (see Media section) | | `card.suggestions` | array | No | Card-level suggestions | | `suggestions` | array | No | Template-level suggestions | | `fallbackSms` | object | No | SMS fallback | | `fallbackWhatsApp` | object | No | WhatsApp fallback | ### CAROUSEL Template Body | Field | Type | Required | Description | |---------------|--------|----------|-------------| | `cardWidth` | string | Yes | `SMALL`, `MEDIUM`, or `UNSPECIFIED` | | `cards` | array | Yes | Array of card objects (min 1) | | `suggestions` | array | No | Template-level suggestions | | `fallbackSms` | object | No | SMS fallback | | `fallbackWhatsApp`| object | No | WhatsApp fallback | ### Media Object | Field | Type | Description | |----------------|--------|-------------| | `height` | string | `SHORT`, `MEDIUM`, `TALL`, `UNSPECIFIED` | | `fileUrl` | string | Public media URL (mutually exclusive with mediaKey) | | `mediaKey` | string | Internal media key (mutually exclusive with fileUrl) | | `thumbnailUrl` | string | Preview URL (optional) | ### Suggestion Types | Type | Description | Required fields | |----------------------|----------------------|-----------------| | `reply` | Quick reply | `text` | | `url` | Opens a link | `text`, `url.url` | | `dial` | Starts a call | `text`, `dial.phoneNumber` | | `locationCoordinates`| Shows location | `text`, `locationCoordinates.latitude`, `.longitude`, `.label` | | `locationQuery` | Searches for location| `text`, `locationQuery.query` | | `calendar` | Calendar event | `text`, `calendar.title`, `.description`, `.startTime`, `.endTime` | ### Fallback Chain - `fallbackWhatsApp`: if RCS not available, sends via WhatsApp - `fallbackSms`: if previous channel fails, sends SMS (required if fallbackWhatsApp is present) ### List Filters | Parameter | Description | |-----------|-------------| | `search` | Full-text search on name/description | | `type` | `TEXT`, `CARD`, `CAROUSEL` | | `enabled` | `0` = disabled only, `1` = enabled only | | `sortBy` | Sort field | | `sortOrder`| `asc` or `desc` | | `limit` | Items per page | | `page` | Page number (0-based) | ### Example: Create TEXT Template ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/rcs/templates" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "rcs_welcome", "description": "Welcome template", "type": "TEXT", "body": { "text": "Ciao {name}! Benvenuto nel nostro servizio.", "suggestions": [ {"type": "reply", "text": "Grazie!"}, {"type": "url", "text": "Visita il sito", "url": {"url": "https://example.com"}} ], "fallbackSms": { "sender": "AZIENDA", "text": "Ciao {name}! Benvenuto. Visita https://example.com" } } }' ``` --- ## WhatsApp Templates Meta-approved messages that can be sent at any time, even outside the 24-hour window. Required to initiate conversations. ### Endpoints | Method | Path | Description | |----------|-----------------------------------------------------------|-------------------| | `GET` | `/message-server/whatsapp/templates` | List templates | | `GET` | `/message-server/whatsapp/templates/{id}` | Template details | | `POST` | `/message-server/whatsapp/templates?phoneNumberId={id}` | Create template | | `PATCH` | `/message-server/whatsapp/templates/{id}` | Update template | | `DELETE` | `/message-server/whatsapp/templates/{id}` | Delete template | ### Template Creation Fields | Field | Type | Required | Description | |--------------------|---------|----------|-------------| | `name` | string | Yes | Template name (snake_case, lowercase, numbers, underscores only) | | `lang` | string | Yes | Language code (e.g. `it`, `en`, `es`) | | `category` | string | Yes | `MARKETING`, `UTILITY`, or `AUTHENTICATION` | | `body` | string | Yes | Body text with `{placeholderName}` placeholders | | `headerText` | string | No | Header text (exclusive with headerFormat) | | `headerFormat` | string | No | Media header: `IMAGE`, `VIDEO`, `DOCUMENT` | | `headerMediaUrl` | string | No | Media URL for header | | `footer` | string | No | Footer text | | `buttons` | array | No | Interactive buttons | | `placeholderFields`| object | No | Tracked link definition | | `trackButtonLinks` | boolean | No | Track clicks on URL buttons | ### Categories | Category | When to use | Examples | |------------------|------------|---------| | `MARKETING` | Promotions, offers, newsletters | Sales, new products, events | | `UTILITY` | Transactional communications | Order confirmation, tracking, reminders | | `AUTHENTICATION` | Identity verification | OTP, verification codes | ### Template Statuses | Status | Meaning | |------------|---------| | `APPROVED` | Approved by Meta, available for sending | | `PENDING` | Awaiting review by Meta | | `REJECTED` | Rejected by Meta | | `PAUSED` | Paused | | `DISABLED` | Disabled | ### Button Types | Field | Type | Required | Description | |---------------|--------|----------|-------------| | `type` | string | Yes | `URL`, `PHONE_NUMBER`, or `QUICK_REPLY` | | `text` | string | Yes | Displayed text | | `url` | string | URL only | URL to open | | `phoneNumber` | string | PHONE_NUMBER only | Number to call | ### Tracked Links In body: Insert `{shortLinkT1}` placeholder and define in `placeholderFields`: ```json { "body": "Ciao {firstName}, clicca qui {shortLinkT1} per la tua offerta.", "placeholderFields": { "WHATSAPP": { "shortLinkT1": "https://example.com/offerta" } } } ``` In buttons: Add `"trackButtonLinks": true` to the request. ### List Filters | Parameter | Description | |----------------|-------------| | `category` | `MARKETING`, `UTILITY`, `AUTHENTICATION` | | `status` | `APPROVED`, `PENDING`, `REJECTED`, `PAUSED`, `DISABLED` | | `phoneNumberId`| Filter by associated number | | `page` | Page number (0-based) | | `limit` | Items per page | ### Updating With `PATCH /templates/{id}` you can update only the fields you want. The template returns to `PENDING` status for re-approval by Meta. ### Example: Create Template with Buttons ```bash curl -X POST "https://lora-api.agiletelecom.com/api/message-server/whatsapp/templates?phoneNumberId=5" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "marketing_welcome", "lang": "it", "category": "MARKETING", "headerText": "Welcome", "body": "Ciao {firstName} siamo contentissimi di averti tra noi", "footer": "Per info visita il sito", "buttons": [ {"type": "URL", "text": "Apri il sito", "url": "https://agiletelecom.com/"}, {"type": "PHONE_NUMBER", "text": "Chiamaci", "phoneNumber": "+39 123123123"}, {"type": "QUICK_REPLY", "text": "Voglio essere contattato"} ] }' ``` --- # Qlara Platform API Path prefix: `/api/partner-gateway/v1/` Manages: contacts, lists, campaigns, webhooks, exports, inbox, media, subscription, social profiles, API keys. --- ## Channel Resources (Senders & Agents) Look up the account's approved senders, RCS agents, and WhatsApp numbers before sending. The returned IDs are used as `sender`, `agentId` / `rcsAgentId` / `fallbackRcs.agentId`, and `phoneNumberId` in send, campaign, and fallback requests. ### List SMS Senders `GET /partner-gateway/v1/sms/senders` Returns the approved alphanumeric/numeric senders usable in the `sender` field (SMS send, SMS fallback, campaigns). ### List RCS Agents `GET /partner-gateway/v1/rcs/agents` Paginated (`limit`, `page`). Returns the account's RCS agents; use the returned agent `id` as `agentId` (RCS send), `rcsAgentId` (campaigns), or `fallbackRcs.agentId` (WhatsApp fallback). ### Get RCS Agent `GET /partner-gateway/v1/rcs/agents/{agentId}` Returns a single RCS agent's details. ### WhatsApp Phone Numbers `GET /partner-gateway/v1/whatsapp/phone-numbers` `GET /partner-gateway/v1/whatsapp/phone-numbers/{phoneNumberId}` The partner-gateway equivalents of the channel-spec `/message-server/whatsapp/phone-numbers` endpoints. Either path returns the WhatsApp Business numbers; use the returned `id` as `phoneNumberId`. --- ## Contacts ### List Contacts `GET /partner-gateway/v1/contacts` Filter by name, phone, email, gender, validity, channel support. Paginated with `page`/`limit` (page object, see Pagination). ### Create Contact `POST /partner-gateway/v1/contacts` Request body fields: - `phone` (string, required) -- Phone number in international format - `firstName` (string) -- First name - `lastName` (string) -- Last name - `fullName` (string) -- Full name - `mainEmail` (string) -- Email address - `gender` (string) -- Gender - `birthDate` (string) -- Date of birth - `city` (string) -- City - `province` (string) -- Province - `cap` (string) -- ZIP/postal code - `nation` (string) -- Nation - `fax` (string) -- Fax number - `address` (string) -- Address - `company` (string) -- Company name - `groupId` (string) -- Group ID ### Get Contact `GET /partner-gateway/v1/contacts/{id}` ### Update Contact `PUT /partner-gateway/v1/contacts/{id}` Same fields as create. ### Delete Contacts `DELETE /partner-gateway/v1/contacts?ids=101,102,103` The contact IDs to delete go in the `ids` query parameter, comma-separated (not in the body). Answers `200 OK` with `true`. ### Upload CSV/Excel `POST /partner-gateway/v1/contacts/upload` Multipart form upload with `file` and `fileName`; `removeHead=true` skips a header row. Imports in the same call: columns are mapped automatically, new contacts are created and existing ones (matched by phone number) are updated. To put them in a list, add them afterwards with `POST /partner-gateway/v1/contacts/list/contacts`. The response counts the contacts: `numContact`, `addedContact`, `updatedContact`, `numError`. ### Upload VCard `POST /partner-gateway/v1/contacts/upload/vcard` Multipart form upload with `file` and `fileName` (a `.vcf` file). Imports in the same call, with the same create-or-update behaviour and the same counts in the response. --- ## Contact Lists ### List Contact Lists `GET /partner-gateway/v1/contacts/list` Paginated. Each list includes `id`, `name`, `description`, `numContacts`, `creationDate`. ### Create Contact List `POST /partner-gateway/v1/contacts/list` Request body: - `name` (string, required) -- List name - `description` (string) -- Description ### Get Contact List `GET /partner-gateway/v1/contacts/list/{id}` ### Update Contact List `PUT /partner-gateway/v1/contacts/list/{id}` ### Delete Contact Lists `DELETE /partner-gateway/v1/contacts/list?ids=7,8` The list IDs go in the `ids` query parameter, comma-separated (not in the body). Answers `200 OK` with `true`. ### Add Contacts to Lists `POST /partner-gateway/v1/contacts/list/contacts` Request body: - `listIds` (array, required) -- List IDs to add contacts to - `contactIds` (array) -- Contact IDs to add (required if `allContacts` is false) - `allContacts` (boolean) -- If true, adds all contacts - `excludeIdsAllContacts` (array) -- Contact IDs to exclude when `allContacts` is true ### Remove Contacts from Lists `DELETE /partner-gateway/v1/contacts/list/contacts` Same body structure as add. ### Delete Contact from List `DELETE /partner-gateway/v1/contacts/list/contacts/{id}` ### Browse List Members `GET /partner-gateway/v1/contacts/list/{listId}/contacts` Paginated with filters for name, phone, email. Supports sorting. --- ## Campaigns ### List Campaigns `GET /partner-gateway/v1/campaigns` Paginated. Filters by status, channel, date range. Includes delivery stats. ### Create Campaign `POST /partner-gateway/v1/campaigns` Request body: | Field | Type | Description | |----------------------|---------|-------------| | `name` | string | Campaign name (max 50 chars) | | `description` | string | Description (max 350 chars) | | `sendingMode` | string | Channel: `SMS`, `RCS`, `RCS_SMS`, `WHATSAPP`, `WHATSAPP_SMS`, `WHATSAPP_RCS`, `WHATSAPP_RCS_SMS` | | `destinationType` | integer | `0` = contact lists, `1` = manual numbers, `2` = both | | `contactListIds` | array | List IDs for recipients (when destinationType is 0 or 2) | | `destinations` | array | Manual phone numbers (when destinationType is 1 or 2) | | `smsSender` | string | SMS sender name/number | | `smsBody` | string | SMS text. Supports `{{placeholder}}` syntax | | `rcsAgentId` | string | RCS agent ID (required for RCS) | | `rcsTemplateId` | integer | RCS template ID | | `whatsappPhoneNumberId` | integer | WhatsApp phone number ID (required for WhatsApp) | | `whatsappTemplateId` | integer | WhatsApp template ID | | `scheduledDate` | string | Schedule date/time | | `readyToSend` | boolean | Mark ready for confirmation | | `placeholderFields` | object | Default placeholder values by name, as a JSON object | ### Get Campaign `GET /partner-gateway/v1/campaigns/{id}` Response includes full delivery stats: `totalDestinations`, `totalSent`, `totalSuccess`, `totalFailed`, `totalRead`, `totalFallback`, per-channel counters (`rcsSent`, `rcsSuccess`, `rcsFailed`, `rcsRead`, `smsSent`, `smsSuccess`, `smsFailed`, `waSent`, `waSuccess`, `waFailed`, `waRead`, etc.). Campaign `status`, in lifecycle order: `DRAFT`, `READY_TO_SEND`, `PRE_BILLED`, `READY`, `EXPLODING`, `EXPLODED`, `BILLED`, `MESSAGE_BILLED`, `STARTED`, `MESSAGE_SENT`, `ENDED`, `ARCHIVED`. `totalPrice` is in the unit of the wallet named by `walletKind`: hundred-millionths of a euro for `EURO`, credits for `CREDITS`. ### Update Campaign `PUT /partner-gateway/v1/campaigns/{id}` Same fields as create (all optional). Allowed only while the campaign is `DRAFT` or `READY_TO_SEND`; afterwards `400`. ### Delete Campaign `DELETE /partner-gateway/v1/campaigns/{id}` Allowed while the campaign is `DRAFT` or `READY_TO_SEND`, or `PRE_BILLED` with a `scheduledDate` still outside the edit window (the reserved amount is released); otherwise `400`. ### Calculate Cost `POST /partner-gateway/v1/campaigns/{id}/calculateGoal` Returns total price based on recipients and channel rates. ### Get Price `GET /partner-gateway/v1/campaigns/{id}/price` ### Confirm / Schedule `PUT /partner-gateway/v1/campaigns/{id}/confirm` Reserves the price on the wallet (`PRE_BILLED`), then sends immediately or at `scheduledDate`. ### Campaign Statistics `GET /partner-gateway/v1/campaigns/stats` Aggregated stats across campaigns. Filter by date range, channel. Returns per-time-bucket stats with `timeBucket`, `sendingMode`, `numMessageCampaigns`, delivery counters. --- ## Webhooks Configure an HTTPS callback URL to receive real-time delivery-status notifications (plus read and inbound-message events on RCS and WhatsApp). ### Endpoints | Method | Path | Description | |----------|--------------------------------------------|-------------| | `POST` | `/partner-gateway/v1/webhooks/delivery-status` | Configure webhook: `201` `{companyId, callbackUrl}`; `409` if one is already configured; `400` if the URL is invalid | | `GET` | `/partner-gateway/v1/webhooks/delivery-status` | Get current config: `200` `{companyId, callbackUrl}`; `404` if none | | `PUT` | `/partner-gateway/v1/webhooks/delivery-status` | Update callback URL (same body as POST): `200` `{companyId, callbackUrl}`; `404` if none; `400` if the URL is invalid | | `DELETE` | `/partner-gateway/v1/webhooks/delivery-status` | Revoke webhook: `204 No Content` (empty body); `404` if none | One URL per account, used for SMS, RCS and WhatsApp alike. There is no webhook id, no `/webhooks/{id}` path, and the response carries only `companyId` and `callbackUrl`. ### Configure Webhook ```bash curl -X POST "https://lora-api.agiletelecom.com/api/partner-gateway/v1/webhooks/delivery-status" \ -H "X-Api-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"callbackUrl": "https://your-server.example.com/webhooks/delivery-status"}' ``` Response: `201 Created` ```json { "companyId": 1, "callbackUrl": "https://your-server.example.com/webhooks/delivery-status" } ``` Only one webhook URL per account. If already configured, POST returns `409 Conflict` and does not replace it. Use PUT to update or DELETE to remove first. The callback URL is validated when you register it (POST and PUT). It must use `https` on port 443 (the default) or 8443, carry no credentials and no `#` fragment, and resolve to a publicly reachable address -- loopback, private, link-local, carrier-grade-NAT and unique-local addresses are refused, as is a host that does not resolve at all. A URL that fails any of these checks is rejected with `400`. ### Callback Delivery - Events are POSTed as JSON (`Content-Type: application/json`); fields that are null are omitted. - Your endpoint must answer `2xx` (any `2xx` counts as success) within 5 seconds (3 seconds to connect). - On failure the platform retries: up to 5 attempts in total, spaced at least 5 minutes apart for SMS and at least 10 minutes apart for RCS and WhatsApp, and only for messages sent in the last 10 days. - Events can arrive more than once and out of order: make processing idempotent, keyed on `eventType` + `messageId` (+ `numPart` for SMS). - Requests carry no signature header. --- ## Webhook Payloads The platform sends HTTP POST callbacks to your configured URL. Each callback carries one event; `eventType` tells you which: | `eventType` | Channels | Sent when | |-------------|--------------------|-----------| | `DELIVERY` | SMS, RCS, WhatsApp | The carrier/platform receipt arrives. In-flight states (`1` accepted, `0` unknown) are never sent | | `READ` | RCS, WhatsApp | The recipient opens the message | | `INBOUND` | RCS, WhatsApp | The end user writes to you | ### SMS DELIVERY Callback ```json { "eventType": "DELIVERY", "channel": "SMS", "messageId": "msg-sms-001", "destination": "+393401234567", "statusCode": 3, "description": "delivered", "eventDate": "2026-04-09T11:30:05+02:00", "price": 0.035, "totalParts": 1, "numPart": 1 } ``` | Field | Type | Description | |---------------|---------|-------------| | `eventType` | string | `DELIVERY` | | `channel` | string | `SMS` | | `messageId` | string | The `customerMessageId` you got at send time (there is no separate `customerMessageId` field). Use it with `GET /partner-gateway/v1/messages/status/{customerMessageId}?channel=SMS` | | `destination` | string | Recipient number | | `statusCode` | integer | Numeric delivery status (see Delivery Status Codes). On SMS: `2` rejected, `3` delivered, `4` expired, `5` deleted, `6` undeliverable | | `description` | string | Lowercase status word matching `statusCode`: `rejected`, `delivered`, `expired`, `deleted`, `undeliverable` | | `eventDate` | string | ISO 8601 with offset | | `price` | number | Message cost | | `totalParts` | integer | Total parts of the concatenated SMS (SMS only) | | `numPart` | integer | Part this event refers to: one event per SMS part (SMS only) | ### RCS DELIVERY Callback Same fields as the SMS DELIVERY event, without `totalParts` and `numPart`: ```json { "eventType": "DELIVERY", "channel": "RCS", "messageId": "msg-rcs-001", "destination": "+393401234567", "statusCode": 3, "description": "delivered", "eventDate": "2026-04-09T11:30:05+02:00", "price": 0.035 } ``` `statusCode` on RCS: `3` delivered, `4` expired, `9` general error, `10` disabled, `11` unsupported. `0` (unknown) is what the polling API shows while an RCS message is in flight; it is never sent as a DELIVERY event. ### RCS READ Callback ```json { "eventType": "READ", "channel": "RCS", "messageId": "msg-rcs-001", "destination": "+393401234567", "eventDate": "2026-04-09T11:31:15+02:00" } ``` A READ event has no `statusCode`, `description`, `price` or `readDate`. The message stays `DELIVERED`. On RCS the polling API's `readDate` is always `null`, so this event is the only way to learn that an RCS message was read. ### RCS INBOUND Callback ```json { "eventType": "INBOUND", "channel": "RCS", "messageId": "", "source": "+393401234567", "destination": "", "receivedDate": "2026-04-09T11:35:00+02:00", "messageType": "TEXT", "text": "Hi, I need help", "mediaKey": null } ``` `source` is the end user's number; `destination` is your RCS agent id. An INBOUND event has `receivedDate` and no `eventDate`. `messageType`: `TEXT`, `IMAGE`, `AUDIO`, `VIDEO`. `mediaKey` is set only on media messages: it contains the key for downloading via `GET /api/files/{mediaKey}`. ### WhatsApp DELIVERY Callback Same structure as RCS DELIVERY with `"channel": "WHATSAPP"`. `statusCode` on WhatsApp: `3` delivered, `4` expired, `9` general error, `10` disabled, `11` unsupported, `12` conversation closed. ### WhatsApp READ Callback ```json { "eventType": "READ", "channel": "WHATSAPP", "messageId": "msg-wa-001", "destination": "+393401234567", "eventDate": "2026-04-09T11:31:15+02:00" } ``` No `statusCode`, `description`, `price` or `readDate`. The message stays `DELIVERED`; on WhatsApp the polling API also shows the read as `readDate`. ### WhatsApp INBOUND Callback ```json { "eventType": "INBOUND", "channel": "WHATSAPP", "messageId": "", "source": "+393401234567", "destination": "", "receivedDate": "2026-04-09T11:35:00+02:00", "messageType": "TEXT", "text": "Hi, I need help", "mediaKey": null } ``` `source` is the end user's number; `destination` is your WhatsApp Business number. `mediaKey` is set only on media messages, for download via `GET /api/files/{mediaKey}`. --- ## Message Delivery Status (Polling) The `customerMessageId` used below is the id returned at send time (the same value the webhook sends as `messageId`). ### Batch Status `GET /partner-gateway/v1/messages/status?channel=SMS&ids=msg-001,msg-002,msg-003` Query parameters (both required): - `channel` -- `SMS`, `RCS` or `WHATSAPP` (case-insensitive). All ids in one request must belong to this channel; make one request per channel. - `ids` -- comma-separated `customerMessageId` values, no spaces (the parameter is `ids`, not `customerMessageIds`). Keep each batch to a few hundred ids. Returns `200` with a plain JSON array of status objects (unordered). Ids that do not match a message for your account and that channel are silently omitted, so the array may be shorter than the list you sent, or empty (still `200`, never `404`). `400` if `channel` or `ids` is missing, or `channel` is unknown. ### Single Message Status `GET /partner-gateway/v1/messages/status/{customerMessageId}?channel=SMS` Query parameters: `channel` (required: `SMS`, `RCS`, `WHATSAPP`). Returns `200` with the status object, `400` if `channel` is missing or unknown, `404` if the message is not found for that channel. ### RCS Message Status `GET /partner-gateway/v1/rcs/messages/{messageId}` `200` with the status object, `404` if not found. ### WhatsApp Message Status `GET /partner-gateway/v1/whatsapp/messages/{messageId}` `200` with the status object, `404` if not found. Both per-channel endpoints are shortcuts over the same delivery-status source as `GET /partner-gateway/v1/messages/status/{customerMessageId}?channel=RCS|WHATSAPP` and return an identical payload: ```json { "customerMessageId": "msg-rcs-001", "channel": "RCS", "destination": "+393401234567", "deliveryStatus": "DELIVERED", "deliveryStatusDescription": "delivered", "sendDate": "2025-03-10T14:30:00+01:00", "deliveryDate": "2025-03-10T14:30:05+01:00", "readDate": null } ``` Status object fields: `customerMessageId`, `channel`, `destination`, `deliveryStatus`, `deliveryStatusDescription` (lowercase word, see Delivery Status Codes), `sendDate`, `deliveryDate` (set only once a receipt arrives), `readDate`. Statuses you can get back: - RCS: `DELIVERED`, `EXPIRED`, `ERROR`, `DISABLED`, `UNSUPPORTED`, `UNKNOWN`. `readDate` is always `null` on RCS: reads arrive only as READ webhook events. - WhatsApp: `DELIVERED`, `EXPIRED`, `ERROR`, `DISABLED`, `UNSUPPORTED`, `CONVERSATION_CLOSED`, `UNKNOWN`. A read message stays `DELIVERED` and gets a `readDate`. - On RCS and WhatsApp, `UNKNOWN` is also the in-flight state (sent, no receipt yet), so seeing it for a while is normal. Prefer the generic endpoint when you track more than one channel: it also looks up SMS (`ACCEPTED`, `REJECTED`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`, `UNKNOWN`). --- ## Message History ### Browse History `GET /partner-gateway/v1/messages/history?channel=SMS&from=2026-08-01&to=2026-08-31&status=DELIVERED&page=0&limit=20` Query parameters: - `channel` (required) -- `SMS`, `RCS` or `WHATSAPP` (case-insensitive). One channel per request. - `from`, `to` (required) -- send-date range, inclusive. `yyyy-MM-dd` (a date alone covers the whole UTC day), `yyyy-MM-ddTHH:mm:ss` (UTC), or ISO 8601 with offset (percent-encode `+` as `%2B`). - `status` (optional) -- one of the 11 `deliveryStatus` names. Which ones can appear depends on the channel: for example `status=ERROR` on SMS always returns nothing (SMS failures are `UNDELIVERABLE`, `REJECTED`, `EXPIRED`, `DELETED`). - `page` (default `0`), `limit` (default `20`). Returns a plain JSON array of status objects (same fields as the status endpoints), possibly empty. The last page is the one with fewer items than `limit`. Missing or malformed `channel`, `from` or `to` answers `400` (not `404`) and the body names the parameter. ### Export History (CSV) `POST /partner-gateway/v1/messages/history/export` Request body: ```json { "startDateTime": "2026-08-01T00:00:00+02:00", "endDateTime": "2026-08-31T23:59:59+02:00", "sender": "MyBrand", "recipient": "+393401234567" } ``` `startDateTime` and `endDateTime` are required (ISO 8601 with offset); `sender` and `recipient` are optional filters. The export covers all channels in one file and has no channel or status filter; only messages sent through the API are included. Answers `202 Accepted` with an empty body. Then poll `GET /partner-gateway/v1/exports` until the export shows `isAvailableForDownload: true`, and call `GET /partner-gateway/v1/exports/{exportId}` for the download URL. --- ## Exports Asynchronous export workflow for large data sets. ### Request Export | Export type | Endpoint | |--------------------|----------| | Contacts | `POST /partner-gateway/v1/exports/contacts` | | Delivery reports | `POST /partner-gateway/v1/exports/delivery-reports` | | Message history | `POST /partner-gateway/v1/messages/history/export` | ### List Exports `GET /partner-gateway/v1/exports` Export statuses: `PENDING`, `COMPLETED`, `FAILED`. Download links expire after a while; `isAvailableForDownload` may then turn back to `false`, and you regenerate the link as below. ### Get Download Link `GET /partner-gateway/v1/exports/{exportId}` Returns a download URL. ### Regenerate Expired Link `POST /partner-gateway/v1/exports/{exportId}` Creates a new download URL for the same export data. --- ## Inbox (Two-way Messaging) ### List Conversations `GET /partner-gateway/v1/inbox/conversations` Parameters: - `channelIds` (string) -- Comma-separated channel IDs - `dateFrom` (string) -- ISO date filter - `orderBy` (string) -- Sort field with +/- prefix (e.g. `-lastMessageDate`) - `unreadOnly` (boolean) -- Only unread conversations ### Send Reply `POST /partner-gateway/v1/inbox/conversations/reply` Sends reply on the same channel the conversation was established on. Accepted asynchronously (202). ### Get Messages `GET /partner-gateway/v1/inbox/conversations/{chatId}/messages` Parameters: - `idFrom` (integer) -- Messages with ID >= this value - `idTo` (integer) -- Messages with ID <= this value ### Mark as Read `PATCH /partner-gateway/v1/inbox/conversations/{chatId}/read` Resets unread counter. Returns 204. ### Archive Conversation `PATCH /partner-gateway/v1/inbox/conversations/{chatId}/archive` Soft operation -- no messages deleted. Returns 204. ### Unarchive Conversation `PATCH /partner-gateway/v1/inbox/conversations/{chatId}/unarchive` Returns 204. ### Get Conversation `GET /partner-gateway/v1/inbox/conversations/{chatId}` Returns a single conversation's metadata, including its current assignee, unread counter, last message preview, and channel. ### List Assignable Users `GET /partner-gateway/v1/inbox/conversations/assignable-users` Lists the team members a conversation can be assigned to. ### Assign Conversation `POST /partner-gateway/v1/inbox/conversations/{chatId}/assignee` Assigns the conversation to a user. One assignee per conversation; assigning replaces any prior assignee. Returns 204. ### Unassign Conversation `DELETE /partner-gateway/v1/inbox/conversations/{chatId}/assignee` Removes the current assignee. Returns 204. --- ## Media ### Upload Media `POST /partner-gateway/v1/media` Multipart form upload. Supports JPEG, PNG, GIF images and MP4 videos. Fields: `file` (binary, required), `name` (string), `description` (string), `folderId` (integer). Returns `201` with an array holding the created media (`id`, `key`, `url`, ...). ### List Media `GET /partner-gateway/v1/media` Paginated. ### Get Media `GET /partner-gateway/v1/media/{id}` ### Delete Media `DELETE /partner-gateway/v1/media/{id}` Returns 204. --- ## Subscription ### Get Active Subscription `GET /api/partner-gateway/v1/subscription` Response: ```json { "planCode": "PRO_MONTHLY", "status": "ACTIVE", "billingPeriodStart": "2025-03-01T00:00:00+01:00", "billingPeriodEnd": "2025-03-31T23:59:59+01:00", "nextRenew": "2025-04-01T00:00:00+01:00", "canceled": false, "nextPlan": null } ``` ### Get Credit `GET /api/partner-gateway/v1/subscription/credit` Returns available messaging credit. --- ## Social Profiles Connected external platform accounts (Facebook, Instagram, LinkedIn, Google, TikTok). Profiles are established via OAuth in the web dashboard. ### List Social Profiles `GET /api/partner-gateway/v1/socials` ### Get Social Profile Detail `GET /api/partner-gateway/v1/socials/{platform}` `platform`: `facebook`, `instagram`, `linkedin`, `google`, `tiktok` (case-insensitive). Returns profile info + linked pages/sub-accounts. --- ## API Keys ### List API Keys `GET /partner-gateway/v1/authentication` Paginated. ### Create API Key `POST /partner-gateway/v1/authentication` Request body: - `userId` (number, required) -- ID of the user the key belongs to - `operations` (array, required) -- Permitted operations. Must be a subset of the operations held by the key making the call: a key cannot grant privileges it does not have. Returns 403 if the requested operations exceed the caller's own scope. - `trafficType` (number, optional) -- Traffic type identifier (0 = standard) There is no `username` field: keys are identified by `id` and `userId`, so the `username` in the response is always `null`. Response includes the full `apiKey` string (only visible at creation time). ### Describe the Current API Key `GET /partner-gateway/v1/authentication/me` Returns the metadata of the key authenticating the request (id, company, user, allowed operations, traffic type, dates). The key is read from the `X-API-Key` header and is never sent in the URL, so the endpoint takes no parameters. It replaces the removed `GET /partner-gateway/v1/authentication/{value}`. ### Delete API Key `DELETE /partner-gateway/v1/authentication/{id}` Returns 204. --- # Channel Comparison | Feature | SMS | RCS | WhatsApp | |----------------------|------------------|------------------|-----------------------| | Rich media | No | Yes | Yes | | Template required | No | Yes | Yes (Meta-approved) | | Delivery receipts | Yes | Yes | Yes | | Read receipts | No | READ webhook event only (polling `readDate` is always `null`) | READ webhook event + `readDate` on polling | | Two-way messaging | Limited | Yes | Yes | | Reach | Universal | Android | WhatsApp users | | Approval process | None | Internal | Meta review | | Content types | Text only | Text, cards, carousels, media, buttons | Text, image, video, audio, document, location, sticker, reaction | ## Fallback Chain The platform supports automatic fallback: WhatsApp -> RCS -> SMS. Configure via `fallbackRcs` and `fallbackSms` fields in send requests. Sending modes for campaigns: `SMS`, `RCS`, `RCS_SMS`, `WHATSAPP`, `WHATSAPP_SMS`, `WHATSAPP_RCS`, `WHATSAPP_RCS_SMS`. --- # Error Codes Reference | Code | Meaning | When it occurs | |-------|------------------------|----------------| | `400` | Bad Request | Invalid body/params. The JSend `data` field names the parameter and the accepted values | | `401` | Unauthorized | Missing/invalid API key or Basic Auth credentials. Body: `{"error": "Invalid API Key"}` | | `403` | Forbidden | API key lacks permission; IP not in whitelist | | `404` | Not Found | Resource doesn't exist; wrong endpoint path or ID | | `409` | Conflict | Resource already exists (e.g. duplicate webhook) | | `422` | Unprocessable Entity | Send endpoints (`POST /partner-gateway/v1/sms/messages`, `/rcs/messages`, `/whatsapp/messages`): the messaging platform refused the message. JSend `fail` body; not retryable as-is (`202` if a fallback channel accepted it) | | `429` | Too Many Requests | Messaging endpoints (`/api/message-server/`) only: rate limit exceeded. Empty body, `Retry-After: 1` | | `500` | Internal Server Error | Server-side issue. Retry with exponential backoff; quote the `X-Request-Id` to support | | `502` | Bad Gateway | Upstream service temporarily unavailable | Qlara Platform errors use the JSend shape described under Error Handling (`{"status": "fail", "data": ...}` for `4xx`, `{"status": "error", "message": "..."}` for `5xx`). Channel-specific validation errors (SMS/RCS/WhatsApp) also return JSend format: ```json { "status": "fail", "data": { "destination": "'destination' can't be blank" } } ``` ## Delivery Status Codes The same codes are used everywhere: `deliveryStatus` (name) and `deliveryStatusDescription` on the polling API (`/messages/status`, `/messages/history`, `/rcs/messages/{id}`, `/whatsapp/messages/{id}`), numeric `statusCode` and `description` on the DELIVERY webhook event. | Code | `deliveryStatus` | Description / webhook `description` | Channels | Meaning | |------|-----------------------|-------------------------------------|--------------------|---------| | `1` | `ACCEPTED` | `accepted` | SMS | Handed to the carrier, no receipt yet. Not final | | `2` | `REJECTED` | `rejected` | SMS | Carrier refused the message. Final | | `3` | `DELIVERED` | `delivered` | SMS, RCS, WhatsApp | Reached the handset. Final | | `4` | `EXPIRED` | `expired` | SMS, RCS, WhatsApp | Validity window elapsed. Final | | `5` | `DELETED` | `deleted` | SMS | Cancelled before delivery. Final | | `6` | `UNDELIVERABLE` | `undeliverable` | SMS | Destination cannot receive it (unreachable/invalid). Final | | `9` | `ERROR` | `general error` | RCS, WhatsApp | Delivery failed. Final | | `10` | `DISABLED` | `disabled` | RCS, WhatsApp | Recipient has the channel switched off. Final | | `11` | `UNSUPPORTED` | `unsupported` | RCS, WhatsApp | Device/number does not support the channel. Final | | `12` | `CONVERSATION_CLOSED` | `conversation closed` | WhatsApp | 24-hour customer-service window closed. Final | | `0` | `UNKNOWN` | `unknown` | SMS, RCS, WhatsApp | No status held for the message | Rules: - There is no `SENT`, `RECEIVED`, `READ`, `FAILED` or `PENDING` delivery status, and codes `7` and `8` do not exist (`EXPIRED` is `4`). - In flight (sent, not yet confirmed): `ACCEPTED` on SMS, `UNKNOWN` on RCS and WhatsApp, which have no intermediate code. Neither is sent as a webhook event. - SMS failures are `REJECTED`, `UNDELIVERABLE`, `EXPIRED`, `DELETED` (never `ERROR`). RCS/WhatsApp failures are `ERROR`, `DISABLED`, `UNSUPPORTED`, `EXPIRED` (+ `CONVERSATION_CLOSED` on WhatsApp). - A read message stays `DELIVERED`. The read shows up as `readDate` on the polling API (WhatsApp only; on RCS `readDate` is always `null`) and as a READ webhook event (RCS and WhatsApp). - `deliveryDate` is set only once a receipt arrives. --- # Quick Reference -- All Endpoints ## Channel Send APIs | Method | Path | Description | |--------|------|-------------| | `POST` | `/message-server/sms/send` | Send SMS (Universal) | | `POST` | `/services/sms/send` | Send SMS (Legacy) | | `GET` | `/services/sms/credit` | Check SMS credit | | `POST` | `/message-server/rcs/send` | Send RCS message | | `POST` | `/message-server/whatsapp/send` | Send WhatsApp message | | `GET` | `/files/{mediaKey}` | Download inbound media file | ## Channel Resources | Method | Path | Description | |--------|------|-------------| | `GET` | `/partner-gateway/v1/sms/senders` | List SMS senders | | `GET` | `/partner-gateway/v1/rcs/agents` | List RCS agents | | `GET` | `/partner-gateway/v1/rcs/agents/{agentId}` | Get RCS agent | | `GET` | `/partner-gateway/v1/whatsapp/phone-numbers` | List WhatsApp numbers (partner-gateway) | | `GET` | `/partner-gateway/v1/whatsapp/phone-numbers/{phoneNumberId}` | Get WhatsApp number (partner-gateway) | ## RCS Templates | Method | Path | Description | |----------|------|-------------| | `GET` | `/message-server/rcs/templates` | List RCS templates | | `POST` | `/message-server/rcs/templates` | Create RCS template | | `GET` | `/message-server/rcs/templates/{id}` | Get RCS template | | `PUT` | `/message-server/rcs/templates/{id}` | Update RCS template | | `DELETE` | `/message-server/rcs/templates/{id}` | Delete RCS template | ## WhatsApp Phone Numbers & Templates | Method | Path | Description | |----------|------|-------------| | `GET` | `/message-server/whatsapp/phone-numbers` | List phone numbers | | `GET` | `/message-server/whatsapp/phone-numbers/{id}` | Get phone number | | `GET` | `/message-server/whatsapp/templates` | List templates | | `POST` | `/message-server/whatsapp/templates?phoneNumberId={id}` | Create template | | `GET` | `/message-server/whatsapp/templates/{id}` | Get template | | `PATCH` | `/message-server/whatsapp/templates/{id}` | Update template | | `DELETE` | `/message-server/whatsapp/templates/{id}` | Delete template | ## Qlara Platform -- Contacts | Method | Path | Description | |----------|------|-------------| | `GET` | `/partner-gateway/v1/contacts` | List contacts | | `POST` | `/partner-gateway/v1/contacts` | Create contact | | `DELETE` | `/partner-gateway/v1/contacts` | Delete contacts (bulk) | | `GET` | `/partner-gateway/v1/contacts/{id}` | Get contact | | `PUT` | `/partner-gateway/v1/contacts/{id}` | Update contact | | `POST` | `/partner-gateway/v1/contacts/upload` | Upload CSV/Excel | | `POST` | `/partner-gateway/v1/contacts/upload/vcard` | Upload VCard | ## Qlara Platform -- Contact Lists | Method | Path | Description | |----------|------|-------------| | `GET` | `/partner-gateway/v1/contacts/list` | List contact lists | | `POST` | `/partner-gateway/v1/contacts/list` | Create list | | `DELETE` | `/partner-gateway/v1/contacts/list` | Delete lists (bulk) | | `GET` | `/partner-gateway/v1/contacts/list/{id}` | Get list | | `PUT` | `/partner-gateway/v1/contacts/list/{id}` | Update list | | `POST` | `/partner-gateway/v1/contacts/list/contacts` | Add contacts to lists | | `DELETE` | `/partner-gateway/v1/contacts/list/contacts` | Remove contacts from lists | | `DELETE` | `/partner-gateway/v1/contacts/list/contacts/{id}` | Remove single contact | | `GET` | `/partner-gateway/v1/contacts/list/{listId}/contacts` | List contacts in list | ## Qlara Platform -- Campaigns | Method | Path | Description | |----------|------|-------------| | `GET` | `/partner-gateway/v1/campaigns` | List campaigns | | `POST` | `/partner-gateway/v1/campaigns` | Create campaign | | `GET` | `/partner-gateway/v1/campaigns/{id}` | Get campaign | | `PUT` | `/partner-gateway/v1/campaigns/{id}` | Update campaign | | `DELETE` | `/partner-gateway/v1/campaigns/{id}` | Delete campaign | | `POST` | `/partner-gateway/v1/campaigns/{id}/calculateGoal` | Calculate cost | | `GET` | `/partner-gateway/v1/campaigns/{id}/price` | Get price | | `PUT` | `/partner-gateway/v1/campaigns/{id}/confirm` | Confirm/schedule | | `GET` | `/partner-gateway/v1/campaigns/stats` | Campaign statistics | ## Qlara Platform -- Webhooks | Method | Path | Description | |----------|------|-------------| | `POST` | `/partner-gateway/v1/webhooks/delivery-status` | Configure webhook | | `GET` | `/partner-gateway/v1/webhooks/delivery-status` | Get webhook config | | `PUT` | `/partner-gateway/v1/webhooks/delivery-status` | Update webhook URL | | `DELETE` | `/partner-gateway/v1/webhooks/delivery-status` | Revoke webhook | ## Qlara Platform -- Exports | Method | Path | Description | |----------|------|-------------| | `GET` | `/partner-gateway/v1/exports` | List exports | | `POST` | `/partner-gateway/v1/exports/contacts` | Export contacts | | `POST` | `/partner-gateway/v1/exports/delivery-reports` | Export delivery reports | | `GET` | `/partner-gateway/v1/exports/{exportId}` | Get download link | | `POST` | `/partner-gateway/v1/exports/{exportId}` | Regenerate expired link | ## Qlara Platform -- Inbox | Method | Path | Description | |----------|------|-------------| | `GET` | `/partner-gateway/v1/inbox/conversations` | List conversations | | `POST` | `/partner-gateway/v1/inbox/conversations/reply` | Send reply | | `GET` | `/partner-gateway/v1/inbox/conversations/{chatId}/messages` | Get messages | | `PATCH` | `/partner-gateway/v1/inbox/conversations/{chatId}/read` | Mark as read | | `PATCH` | `/partner-gateway/v1/inbox/conversations/{chatId}/archive` | Archive | | `PATCH` | `/partner-gateway/v1/inbox/conversations/{chatId}/unarchive` | Unarchive | | `GET` | `/partner-gateway/v1/inbox/conversations/{chatId}` | Get single conversation | | `GET` | `/partner-gateway/v1/inbox/conversations/assignable-users` | List assignable users | | `POST` | `/partner-gateway/v1/inbox/conversations/{chatId}/assignee` | Assign conversation | | `DELETE` | `/partner-gateway/v1/inbox/conversations/{chatId}/assignee` | Unassign conversation | ## Qlara Platform -- Media | Method | Path | Description | |----------|------|-------------| | `POST` | `/partner-gateway/v1/media` | Upload media | | `GET` | `/partner-gateway/v1/media` | List media | | `GET` | `/partner-gateway/v1/media/{id}` | Get media | | `DELETE` | `/partner-gateway/v1/media/{id}` | Delete media | ## Qlara Platform -- Message History & Status | Method | Path | Description | |----------|------|-------------| | `GET` | `/partner-gateway/v1/messages/history` | Browse history | | `POST` | `/partner-gateway/v1/messages/history/export` | Export history (CSV) | | `GET` | `/partner-gateway/v1/messages/status` | Batch status | | `GET` | `/partner-gateway/v1/messages/status/{customerMessageId}` | Single status | | `GET` | `/partner-gateway/v1/rcs/messages/{messageId}` | RCS message status | | `GET` | `/partner-gateway/v1/whatsapp/messages/{messageId}` | WhatsApp message status | ## Qlara Platform -- Subscription | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/partner-gateway/v1/subscription` | Active subscription | | `GET` | `/api/partner-gateway/v1/subscription/credit` | Credit balance | ## Qlara Platform -- Social Profiles | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/partner-gateway/v1/socials` | List social profiles | | `GET` | `/api/partner-gateway/v1/socials/{platform}` | Social profile detail | ## Qlara Platform -- API Keys | Method | Path | Description | |----------|------|-------------| | `GET` | `/partner-gateway/v1/authentication` | List API keys | | `POST` | `/partner-gateway/v1/authentication` | Create API key | | `GET` | `/partner-gateway/v1/authentication/me` | Describe the current key | | `DELETE` | `/partner-gateway/v1/authentication/{id}` | Delete API key |