openapi: 3.1.0 info: title: MessageEagle Platform API version: "1.0.0" description: | The MessageEagle Platform API lets you send SMS and Voice messages, deliver and verify one-time passwords (OTP), and manage approved sender names. All endpoints are served under the platform base path `/api/v1/app1/platform` and authenticated with two headers: | Header | Purpose | | ------------- | ---------------------------------------------------- | | `API-Key` | Platform API key issued to your application | | `X-TenantID` | The organization the request runs in | Some routes accept a signed-in user's `Authorization: Bearer` token in place of `API-Key`, and the organization-scoped routes take the organization from the path instead of `X-TenantID`. Each operation states what it requires. Requests and responses are JSON. Bulk send endpoints return **per-item** status — a single recipient failing does not fail the whole call. contact: name: MessageEagle Support url: https://messageeagle.com servers: - url: https://api.messageeagle.com/api/v1/app1/platform description: Production - url: https://sandbox.messageeagle.com/api/v1/app1/platform description: Sandbox security: - APIKey: [] XTenantID: [] tags: - name: SMS description: Send transactional, broadcast, and EagleEye SMS. - name: Verify description: Send and verify one-time passwords over SMS. - name: Sender Names description: Register and manage approved sender names. - name: Voice description: Voice OTP, broadcasts, agent dialing, sessions, and IVR. - name: Place Calls description: Call a number — ring it, speak a message, connect an agent, or run a survey. - name: Live Calls description: Control a call that is already in progress. - name: Voice Agents description: Agent configurations and the speech provider/voice catalog. - name: Webhook Deliveries description: Outbound webhook history, replay, and endpoint testing. - name: Desk Phones description: Site enrollment tokens for zero-touch desk-phone provisioning. paths: /sms/send_messages/message: post: tags: [SMS] summary: Send transactional SMS operationId: sendMessage description: Send one or more transactional messages. Pass `transaction_uuid` per item to enable delivery-report webhooks and later tracking. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SendMessageRequest" } example: check_spam_msg_mode: not_check data: - sender_name: MyBrand operator: ais message: "Hi {{first_name}}, your order has shipped." phone_number: "+66886578553" first_name: Mali transaction_uuid: 018f7b2a-7c3e-7a1b-9c2d-1a2b3c4d5e6f responses: "200": description: Per-recipient send report. content: application/json: schema: { $ref: "#/components/schemas/SendMessageResponse" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } /worlddata/countries: get: tags: [World Data] summary: List country dial codes operationId: listWorldDataCountries description: | The E.164 calling code for every country, paginated. Unlike the send endpoints this is reference data — it is the same for every caller, changes a few times a year, and is safe to cache. Authenticated with the `API-Key` header rather than `X-Token`. security: - APIKey: [] parameters: - { in: query, name: page, schema: { type: integer, default: 1 }, description: "1-based page number. A page past the end returns an empty list, not a 404." } - { in: query, name: limit, schema: { type: integer, default: 100, maximum: 250 }, description: "Rows per page. Values above 250 are capped; unparseable values fall back to the default." } responses: "200": description: A page of countries. content: application/json: schema: { $ref: "#/components/schemas/WorldDataCountryPage" } example: object: list data: - { code: TH, dial_code: "+66", name: Thailand } - { code: SG, dial_code: "+65", name: Singapore } page: 1 limit: 100 total_items: 230 total_pages: 3 "401": $ref: "#/components/responses/Unauthorized" /sms/broadcast: post: tags: [SMS] summary: Send one message to many recipients operationId: sendSmsBroadcast description: | Fans one message out to a recipient list as a single batch. This route is a deployment opt-in: it is not mounted unless the deployment sets `ME_SMS_PRODUCER_ENABLED`, so an environment without it answers `404` rather than an error you can act on. Check with your operator before building against it. security: - APIKey: [] requestBody: required: true content: application/json: schema: type: object required: [operator, sender_name, message, recipients] properties: operator: { type: string, enum: [ais, dtac, true_corp], description: "Carrier to send through." } sender_name: { type: string, description: "An approved sender name." } message: { type: string } recipients: { type: array, items: { type: string }, description: "E.164 numbers." } idempotency_key: { type: string, description: "Dedupes retries — a replay with the same key returns the original response instead of re-sending." } validity_seconds: { type: integer, description: "Ask the carrier to drop the message if undelivered within this window. Honoured by DTAC and True; AIS has no expiry element, so it is ignored there." } responses: "200": description: The batch was accepted. content: application/json: schema: { $ref: "#/components/schemas/BroadcastBatch" } "401": { $ref: "#/components/responses/Unauthorized" } "404": description: The producer surface is not enabled on this deployment. /sms/broadcast/{batchId}: get: tags: [SMS] summary: Delivery breakdown for a batch operationId: getSmsBatchStatus security: - APIKey: [] parameters: - { in: path, name: batchId, required: true, schema: { type: string } } responses: "200": description: Counts per delivery state. content: application/json: schema: type: object properties: batch_id: { type: string } total: { type: integer } delivered: { type: integer } sent: { type: integer } failed: { type: integer } timeout: { type: integer } pending: { type: integer } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } /telephony/call/dial: post: tags: [Place Calls] summary: Call a number with nothing played operationId: placeDialCall description: | Rings `destination` from your caller number. Use the returned `call_uuid` to park or transfer the call. Queued unless `mode` is "instant". security: - APIKey: [] XTenantID: [] parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/DialCallRequest" } responses: "202": description: Queued. content: application/json: schema: { $ref: "#/components/schemas/PlaceCallResponse" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "429": description: Every line is busy (`mode` "instant" only). /telephony/call/tts: post: tags: [Place Calls] summary: Call a number and speak a message operationId: placeTtsCall description: | Speaks `tts` when the callee answers, then hangs up. security: - APIKey: [] XTenantID: [] parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/TTSCallRequest" } responses: "202": description: Queued. content: application/json: schema: { $ref: "#/components/schemas/PlaceCallResponse" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "429": description: Every line is busy (`mode` "instant" only). /telephony/call/agent: post: tags: [Place Calls] summary: Call a number and connect an AI agent operationId: placeAgentCall description: | Connects the callee to a saved agent (`agent.id`); `system_prompt`, `voice` and `language` override it for this call. With `gather`, the agent asks for the fields in `gather.parameters` and the answers arrive on the `call.ai_gather.ended` webhook and at `gather.callback_url`. security: - APIKey: [] XTenantID: [] parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AgentCallRequest" } responses: "201": description: Ringing. content: application/json: schema: { $ref: "#/components/schemas/PlaceCallResponse" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "429": description: Every line is busy (`mode` "instant" only). /telephony/call/survey: post: tags: [Place Calls] summary: Call a number and run a survey operationId: placeSurveyCall description: | `survey.mode` "dtmf": a voice reads each of `survey.questions` and the callee presses one key per question. Queued (202). Each answer is stored as `.` for survey reports. `survey.mode` "agent" (default): an AI agent asks for the fields in `survey.parameters` by voice and hangs up once every required answer is valid. Rings now (201). Answers arrive on the `call.ai_gather.ended` webhook and, when every required answer is valid, at `survey.callback_url`, signed in `X-webhook-signature` (base64 HMAC-SHA256 of the raw body, keyed with `call.gather.callback_secret`). security: - APIKey: [] XTenantID: [] parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SurveyCallRequest" } responses: "201": description: Ringing (agent mode). content: application/json: schema: { $ref: "#/components/schemas/PlaceCallResponse" } "202": description: Queued (dtmf mode). content: application/json: schema: { $ref: "#/components/schemas/PlaceCallResponse" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "429": description: Every line is busy (`mode` "instant" only). /telephony/call/flow: post: tags: [Place Calls] summary: Call a number with a voice flow (not available yet) operationId: placeFlowCall description: | Not available yet: the call rings the callee, but the server does not run the flow. Build on dial, tts, agent or survey for now. security: - APIKey: [] XTenantID: [] parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/FlowCallRequest" } responses: "201": description: Ringing. content: application/json: schema: { $ref: "#/components/schemas/PlaceCallResponse" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "429": description: Every line is busy (`mode` "instant" only). /telephony/call/batch: post: tags: [Place Calls] summary: Call many numbers operationId: placeBatchCall description: | Places one call per destination with the same caller and message. A number that could not be queued carries an `error`; the rest still go out. security: - APIKey: [] XTenantID: [] parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/BatchCallRequest" } responses: "202": description: Queued. content: application/json: schema: type: object properties: queued: { type: integer } total: { type: integer } calls: type: array items: type: object properties: destination: { type: string } call: { $ref: "#/components/schemas/PlacedCall" } error: { type: string } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } /telephony/call/{callUuid}: get: tags: [Place Calls] summary: Read a call's status operationId: getCallStatus security: - APIKey: [] XTenantID: [] parameters: - { in: path, name: callUuid, required: true, schema: { type: string }, description: "`call.call_uuid` from the place reply." } responses: "200": description: The call's current status. content: application/json: schema: { $ref: "#/components/schemas/CallStatus" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } /voice/calls/{roomName}/hold: post: tags: [Live Calls] summary: Hold or resume a live call operationId: holdCall description: | Hold mutes both legs and plays hold music. The mute half applies even when no hold clip is configured, so a held caller never hears the agent's room. Send `muted: false` to resume. security: - APIKey: [] XTenantID: [] parameters: - { in: path, name: roomName, required: true, schema: { type: string }, description: "LiveKit room name of the call in progress." } requestBody: required: true content: application/json: schema: type: object properties: muted: { type: boolean, description: "true holds, false resumes." } participant_identity: { type: string, description: "Narrow the hold to one participant." } track_sid: { type: string, description: "Narrow the hold to one track." } holder_identity: { type: string, description: "Recorded in the call log." } responses: "200": description: The resulting hold state. content: application/json: schema: type: object properties: status: { type: string } held: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } /voice/calls/{roomName}/mode: post: tags: [Live Calls] summary: Switch supervisor listening mode operationId: setCallMode security: - APIKey: [] XTenantID: [] parameters: - { in: path, name: roomName, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object required: [mode] properties: mode: { type: string, description: "Supervisor mode to apply." } identity: { type: string, description: "The supervisor the mode applies to." } exclude_identities: { type: array, items: { type: string }, description: "Participants the mode must not affect." } responses: "200": description: The applied mode. content: application/json: schema: { type: object, properties: { mode: { type: string } } } "401": { $ref: "#/components/responses/Unauthorized" } /voice/calls/{roomName}/hangup: post: tags: [Live Calls] summary: End a live call operationId: hangupCall description: | Deletes the room and seals the session. For a call that is still RINGING this is the wrong route — the room does not exist yet. security: - APIKey: [] XTenantID: [] parameters: - { in: path, name: roomName, required: true, schema: { type: string } } requestBody: required: false content: application/json: schema: type: object properties: participant_identity: { type: string } responses: "200": description: The call was ended. content: application/json: schema: { type: object, properties: { status: { type: string } } } "401": { $ref: "#/components/responses/Unauthorized" } /agents/configs: get: tags: [Voice Agents] summary: List agent configurations operationId: listAgentConfigs security: [{ APIKey: [], XTenantID: [] }] responses: "200": description: The caller's agent configurations. content: application/json: schema: { type: array, items: { type: object, additionalProperties: true } } "401": { $ref: "#/components/responses/Unauthorized" } post: tags: [Voice Agents] summary: Create an agent configuration operationId: createAgentConfig security: [{ APIKey: [], XTenantID: [] }] requestBody: required: true content: application/json: schema: { type: object, additionalProperties: true } responses: "200": description: The created configuration. content: application/json: schema: { type: object, additionalProperties: true } "401": { $ref: "#/components/responses/Unauthorized" } /agents/configs/{documentId}: parameters: - { in: path, name: documentId, required: true, schema: { type: string } } get: tags: [Voice Agents] summary: Get one agent configuration operationId: getAgentConfig security: [{ APIKey: [], XTenantID: [] }] responses: "200": description: The configuration document. content: application/json: schema: { type: object, additionalProperties: true } "404": { $ref: "#/components/responses/NotFound" } put: tags: [Voice Agents] summary: Replace an agent configuration operationId: upsertAgentConfig security: [{ APIKey: [], XTenantID: [] }] requestBody: required: true content: application/json: schema: { type: object, additionalProperties: true } responses: "200": description: The stored configuration. content: application/json: schema: { type: object, additionalProperties: true } delete: tags: [Voice Agents] summary: Delete an agent configuration operationId: deleteAgentConfig security: [{ APIKey: [], XTenantID: [] }] responses: "200": { description: The configuration was deleted. } "404": { $ref: "#/components/responses/NotFound" } /voice/providers: get: tags: [Voice Agents] summary: List speech providers operationId: listVoiceProviders description: | Which providers are available follows the vendor keys configured for the deployment, so this is a runtime question rather than a constant. Read it instead of hard-coding a provider list. security: [{ APIKey: [], XTenantID: [] }] responses: "200": description: Providers available on this deployment. content: application/json: schema: type: object properties: data: { type: array, items: { type: object, additionalProperties: true } } "401": { $ref: "#/components/responses/Unauthorized" } /voice/providers/{provider}/voices: get: tags: [Voice Agents] summary: List a provider's voices operationId: listProviderVoices security: [{ APIKey: [], XTenantID: [] }] parameters: - { in: path, name: provider, required: true, schema: { type: string } } responses: "200": description: Selectable voices. content: application/json: schema: type: object properties: data: type: array items: type: object properties: id: { type: string } name: { type: string } gender: { type: string } language: { type: string } description: { type: string } preview: { type: boolean, description: "Whether THIS voice can be rendered to a sample here. Resolved per request because it follows a vendor key — when false, disable play rather than substituting another voice." } "404": { $ref: "#/components/responses/NotFound" } /organization/{organizationId}/webhook-deliveries: get: tags: [Webhook Deliveries] summary: Webhook delivery history operationId: listWebhookDeliveries description: "`organizationId` is the same value sent as `X-TenantID`." security: [{ APIKey: [], XTenantID: [] }] parameters: - { in: path, name: organizationId, required: true, schema: { type: string } } - { in: query, name: endpoint_id, schema: { type: string } } - { in: query, name: event_id, schema: { type: string } } - { in: query, name: status, schema: { type: string } } - { in: query, name: q, schema: { type: string }, description: "Free-text search." } - { in: query, name: limit, schema: { type: integer } } - { in: query, name: offset, schema: { type: integer } } responses: "200": description: Delivery history. content: application/json: schema: { type: object, additionalProperties: true } "401": { $ref: "#/components/responses/Unauthorized" } /organization/{organizationId}/webhook-deliveries/{deliveryId}/replay: post: tags: [Webhook Deliveries] summary: Replay one delivery operationId: replayWebhookDelivery security: [{ APIKey: [], XTenantID: [] }] parameters: - { in: path, name: organizationId, required: true, schema: { type: string } } - { in: path, name: deliveryId, required: true, schema: { type: string } } responses: "200": description: The replay result. content: application/json: schema: { type: object, additionalProperties: true } "404": { $ref: "#/components/responses/NotFound" } /organization/{organizationId}/voice/enrollment-tokens: get: tags: [Desk Phones] summary: List site enrollment tokens operationId: listEnrollmentTokens description: "`organizationId` is the same value sent as `X-TenantID`." security: [{ APIKey: [], XTenantID: [] }] parameters: - { in: path, name: organizationId, required: true, schema: { type: string } } responses: "200": description: The organization's tokens, newest first. content: application/json: schema: { type: object, additionalProperties: true } "401": { $ref: "#/components/responses/Unauthorized" } post: tags: [Desk Phones] summary: Create a site enrollment token operationId: createEnrollmentToken description: | Mints the token a site's DHCP option 66 URL carries. One token serves a whole branch and is read by handsets nobody has registered yet. The token is **not a credential**: the phone-facing route it unlocks never returns SIP passwords, and it can only ever create a device row for a MAC the organization does not already hold. security: [{ APIKey: [], XTenantID: [] }] parameters: - { in: path, name: organizationId, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object required: [org_node_uuid] properties: org_node_uuid: type: string description: The site (voice org node) handsets enrolling with this token belong to. label: type: string description: Operator-facing name, e.g. the branch or rack. max_devices: type: integer description: "Cap on devices this token may create. `0` is unlimited." expires_at: type: string format: date-time responses: "200": description: The created token. content: application/json: schema: { type: object, additionalProperties: true } "401": { $ref: "#/components/responses/Unauthorized" } /organization/{organizationId}/voice/enrollment-tokens/{uuid}: delete: tags: [Desk Phones] summary: Revoke a site enrollment token operationId: revokeEnrollmentToken description: | Revoking stops the token enrolling new handsets. It does **not** disturb devices already enrolled with it, and it is not a delete — the token row is kept so the audit trail still resolves. security: [{ APIKey: [], XTenantID: [] }] parameters: - { in: path, name: organizationId, required: true, schema: { type: string } } - { in: path, name: uuid, required: true, schema: { type: string } } responses: "200": description: The token is revoked. content: application/json: schema: { type: object, additionalProperties: true } "404": { $ref: "#/components/responses/NotFound" } /webhook-test: post: tags: [Webhook Deliveries] summary: Test an endpoint operationId: testWebhookEndpoint description: | Fires a one-off request at a URL and reports what it answered, so an endpoint can be checked before real events are pointed at it. security: [{ APIKey: [], XTenantID: [] }] requestBody: required: true content: application/json: schema: type: object required: [url, method] properties: url: { type: string, format: uri } method: { type: string, enum: [GET, POST, PUT, PATCH, DELETE, HEAD] } headers: type: array items: type: object properties: key: { type: string } value: { type: string } body: { description: "Arbitrary JSON sent as the request body." } responses: "200": description: What the endpoint answered. content: application/json: schema: type: object properties: success: { type: boolean, description: "The request COMPLETED — not that the endpoint accepted it. Check status too." } status: { type: integer } statusText: { type: string } headers: { type: object, additionalProperties: { type: string } } data: { description: "The endpoint's decoded response body." } "401": { $ref: "#/components/responses/Unauthorized" } /sms/otp: post: tags: [Verify] summary: Send a one-time password over SMS operationId: sendSmsOtp description: | Sends an OTP and, in `auto` mode, owns its whole lifecycle. `mode` decides who owns the code: | Mode | Who mints the code | Verifiable here | | ---------- | ------------------ | --------------- | | `auto` | MessageEagle | Yes | | `provided` | You | No | In `auto` mode only a peppered hash of the code is stored, so the code is never returned and never recoverable — show the user `ref_code`, not the code. In `provided` mode nothing is stored at all: this is transport only, and you verify the code yourself. Sender name and operator are the SAME contract as [SMS](/en/capabilities/sms) — an OTP is routed, billed and refunded by the ordinary SMS pipeline, not a parallel one. This route is a deployment opt-in: it is not mounted unless the deployment sets `ME_SMS_PRODUCER_ENABLED`, so an environment without it answers `404`. security: - APIKey: [] parameters: - in: header name: Idempotency-Key required: false schema: { type: string } description: | Dedupes a client retry — a replay returns the ORIGINAL OTP rather than minting a new one. Reusing a key with a different body is rejected, because replaying the first response would report a code as delivered that never was. requestBody: required: true content: application/json: schema: type: object required: [mode, operator, sender_name, recipient] properties: mode: { type: string, enum: [auto, provided] } operator: type: string enum: [ais, dtac, true_corp, smart] description: | Carrier to send through, or `smart` to let the SMS layer choose. `smart` needs routing configured on the deployment; without it the send answers `503`. sender_name: type: string description: | A sender name approved for your organization. List yours at `GET /organization/{organizationId}/sender-names`; an unapproved name is rejected with `403`, not silently substituted. That list also carries the platform default (`is_platform_default: true`) when the deployment has one configured — not every deployment does, so check rather than assume there is a name you can use without approval. recipient: { type: string, description: "E.164 number." } message_template: type: string description: | Must contain `{{otp}}`; `{{ref_code}}` is optional. Omitted in `auto` mode, the platform default template is used. Mutually exclusive with `message_template_id`. message_template_id: type: string description: | Id of a saved template owned by your organization (see `/template-message/organization/{organizationId}`). Resolved to its text before the message is rendered, so editing the template later cannot rewrite a message already sent — or a resend of it. Must contain `{{otp}}`. Mutually exclusive with `message_template`. config: type: object description: "`auto` mode only — it governs code generation, which is yours in `provided` mode." properties: length: { type: integer, minimum: 4, maximum: 10, description: "Default 6." } type: { type: string, enum: [numeric, alphanumeric], description: "Default numeric." } validity_seconds: type: integer minimum: 1 maximum: 3600 description: | Bounds how long the code is accepted AND is passed to the carrier as the message validity window, so a code cannot arrive after it stopped working. otp: { type: string, description: "`provided` mode only — the code you generated. Never stored, never logged." } ref_code: { type: string, description: "`provided` mode only — your own reference, rendered as `{{ref_code}}`." } validity_seconds: type: integer description: | `provided` mode only, and the CARRIER window only: there is no session to expire, because you own verification. responses: "200": description: The OTP was accepted for delivery. content: application/json: schema: { $ref: "#/components/schemas/OtpSession" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": description: The sender name is not approved for this organization, or the project has no SMS credential. "429": description: A per-workspace or per-project quota was exceeded. "404": description: The producer surface is not enabled on this deployment. /sms/otp/{otpId}/verify: post: tags: [Verify] summary: Verify an auto-mode OTP operationId: verifySmsOtp description: | Checks a code against the session named in the path. A wrong, expired or locked code is a **`200` with `verified: false`**, not a `4xx`: the request was well formed, and your UI needs the status and the remaining attempt count to tell the user what to do next. `provided` mode has no session, so there is nothing to verify here. security: - APIKey: [] parameters: - in: path name: otpId required: true schema: { type: string } description: The `id` returned when the OTP was sent. requestBody: required: true content: application/json: schema: type: object required: [otp] properties: otp: { type: string, description: "The code the user entered." } responses: "200": description: The verification outcome — including a failed one. content: application/json: schema: { $ref: "#/components/schemas/OtpVerifyResult" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } /sms/otp/{otpId}/resend: post: tags: [Verify] summary: Resend an auto-mode OTP operationId: resendSmsOtp description: | Issues a NEW code for the same recipient and invalidates the previous session, which the response names in `replaces`. The original send's shape is reproduced — sender name, operator, code length, code type and the message text as it was RESOLVED at first send. A template edited in between does not change the resent message. security: - APIKey: [] parameters: - in: path name: otpId required: true schema: { type: string } responses: "200": description: A replacement OTP was accepted for delivery. content: application/json: schema: { $ref: "#/components/schemas/OtpSession" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } "429": description: A per-workspace or per-project quota was exceeded. /sms/otp/{otpId}: get: tags: [Verify] summary: Read an OTP session operationId: getSmsOtp description: | Returns the session's state. It never carries the code or its hash — there is no endpoint that can give you the code back. security: - APIKey: [] parameters: - in: path name: otpId required: true schema: { type: string } responses: "200": description: The session. content: application/json: schema: { $ref: "#/components/schemas/OtpSessionDetail" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } /otp/verify: post: tags: [Verify] summary: Verify a one-time password operationId: verifyOtp description: | Verifies a code and, when `purpose` is supplied, records a claim for the phone number in that namespace. This surface VERIFIES a code the platform issued; it does not generate or deliver one. `ref_code` is the reference returned when the code was created, and it KEYS the stored code — so a code you generated and sent over your own SMS cannot be verified here: the lookup finds nothing and the response is `OTP expired or not found`, not `invalid code`. The API key resolves to exactly one project and the claim is recorded against it; a key that does not resolve to a project is rejected. security: - APIKey: [] requestBody: required: true content: application/json: schema: type: object required: [phone_number, code, ref_code] properties: phone_number: { type: string, description: "E.164." } code: { type: string } ref_code: { type: string, description: "The reference issued when the code was created." } email: { type: string } purpose: { type: string, description: 'Optional claim namespace, e.g. "signup".' } responses: "200": description: The verification result. content: application/json: schema: type: object properties: is_valid: { type: boolean } status: { type: string } message: { type: string } is_new: type: boolean description: | Whether this number was claimed for this `purpose` for the first time. ABSENT when there was nothing to answer — no `purpose` was sent, or the claim store failed. Absent is NOT false: a client gating a reward on `!is_new` would pay out on every error instead of none. "401": { $ref: "#/components/responses/Unauthorized" } /organization/{organizationId}/sender-names: parameters: - { in: path, name: organizationId, required: true, schema: { type: string }, description: "Same value as the X-TenantID header." } get: tags: [Sender Names] summary: List sender names operationId: listSenderNames security: [{ BearerAuth: [] }] responses: "200": description: The organization's sender names. content: application/json: schema: type: object properties: data: { type: array, items: { type: object, additionalProperties: true } } post: tags: [Sender Names] summary: Register a sender name operationId: createSenderName description: | Registration submits a KYC dossier: carriers need to know who is behind the name, not only what it is. security: [{ BearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [sender_name, usage_type] properties: sender_name: { type: string } usage_type: { type: string, description: "General / Transactional / Promotional / OTP." } business_name: { type: string } contact_person: { type: string } country: { type: string, description: "ISO 3166-1 alpha-2." } email: { type: string, format: email } name: { type: string } nationality: { type: string } phone_number: { type: string } purpose: { type: string } company_registration_certificate: { type: string, description: "URL to the document." } brand_ownership_proof: { type: string, description: "URL to the document." } responses: "200": description: The registered sender name. content: application/json: schema: { type: object, additionalProperties: true } /organization/{organizationId}/sender-names/availability: get: tags: [Sender Names] summary: Check whether a sender name is available operationId: checkSenderNameAvailability security: [{ BearerAuth: [] }] parameters: - { in: path, name: organizationId, required: true, schema: { type: string } } - { in: query, name: sender_name, required: true, schema: { type: string } } responses: "200": description: Availability for the requested name. content: application/json: schema: { type: object, additionalProperties: true } /organization/{organizationId}/sender-names/{documentId}: parameters: - { in: path, name: organizationId, required: true, schema: { type: string } } - { in: path, name: documentId, required: true, schema: { type: string } } put: tags: [Sender Names] summary: Update the dossier operationId: updateSenderName description: | A partial patch of the dossier. `sender_name`, `obj_status`, `is_active` and `approved_operators` stay server-controlled — a changed name needs a fresh KYC review, and the rest are review outcomes. Register a new name to change the name itself. security: [{ BearerAuth: [] }] requestBody: required: true content: application/json: schema: { type: object, additionalProperties: true } responses: "200": description: The updated sender name. content: application/json: schema: { type: object, additionalProperties: true } delete: tags: [Sender Names] summary: Delete a sender name operationId: deleteSenderName security: [{ BearerAuth: [] }] responses: "200": { description: The sender name was deleted. } "404": { $ref: "#/components/responses/NotFound" } components: securitySchemes: XTenantID: type: apiKey in: header name: X-TenantID description: Tenant scope identifier. BearerAuth: type: http scheme: bearer description: | A signed-in user's access token, as `Authorization: Bearer `. Sender-name management takes this rather than an API key: it is authorized against the caller's membership of the organization named in the path, so there is no `X-TenantID` to send. APIKey: type: apiKey in: header name: API-Key description: | Platform API key from the developer console. A key carries its OWNER's permissions: the request is authorized as the key's owner against the organization in `X-TenantID`. responses: BadRequest: description: The request was malformed. content: application/json: schema: { $ref: "#/components/schemas/Error" } Unauthorized: description: Missing or invalid credentials. content: application/json: schema: { $ref: "#/components/schemas/Error" } NotFound: description: The resource was not found. content: application/json: schema: { $ref: "#/components/schemas/Error" } parameters: IdempotencyKey: in: header name: Idempotency-Key required: false schema: { type: string } description: | Send one per call you place. A retry with the same key returns the first call instead of dialing again. schemas: BroadcastBatch: type: object description: The outcome of fanning one message out to a recipient list. properties: batch_id: type: string description: Correlates the fan-out across message history. accepted: type: integer description: Recipients whose message was queued for the carrier. rejected: type: integer description: Recipients dropped before queueing — blank or duplicate number, or a publish error. undeliverable_chars: type: string description: | Codepoints the carrying operator is known to destroy, as "U+1F381 U+1D7EE". Present only when there are some. The loss is otherwise invisible: the carrier accepts the message, the unit is billed, and the handset shows nothing there. Substitute a character that survives if this is set. OtpSession: type: object description: | An accepted OTP. The code is absent in both modes — in `auto` because only its hash was kept, in `provided` because echoing a secret you already hold buys nothing and puts it in one more log. properties: id: type: string description: Names the session in the verify, resend and read routes. mode: { type: string, enum: [auto, provided] } recipient: { type: string } ref_code: type: string description: The reference to show the user. Not a secret, and not the code. expires_at: type: string format: date-time description: "`auto` mode only — `provided` mode has no session to expire." status: { type: string } message_id: type: string description: Correlates with SMS delivery history. replaces: type: string description: Set by resend — the session this one invalidated. OtpVerifyResult: type: object properties: id: { type: string } verified: { type: boolean } status: { type: string } verified_at: type: string format: date-time description: Present only on the transition to verified. attempts_remaining: type: integer description: | Present only while attempts remain. ABSENT on a locked session, which is NOT the same as `0` — a UI rendering "N attempts left" should show nothing there rather than "0 left". OtpSessionDetail: type: object properties: id: { type: string } mode: { type: string, enum: [auto, provided] } recipient: { type: string } ref_code: { type: string } status: { type: string } message_id: { type: string } expires_at: { type: string, format: date-time } verified_at: { type: string, format: date-time } created_at: { type: string, format: date-time } replaces: { type: string } WorldDataCountry: type: object properties: code: type: string description: ISO-3166 alpha-2 code, uppercase. example: TH dial_code: type: string description: | E.164 calling code including the leading plus. NANP territories carry their full prefix (`+1684` for American Samoa), not a bare `+1` — a picker that offered `+1` there would build unroutable numbers. example: "+66" name: type: string description: English display name. example: Thailand WorldDataCountryPage: type: object properties: object: { type: string, example: list } data: type: array items: { $ref: "#/components/schemas/WorldDataCountry" } page: { type: integer, description: Echoes the requested page. } limit: { type: integer, description: Echoes the applied limit after capping. } total_items: { type: integer, example: 230 } total_pages: { type: integer, example: 3 } Error: type: object properties: code: { type: string, example: invalid_request } message: { type: string, example: "phone_number is required" } type: { type: string } Address: type: object properties: address_line_1: { type: string } address_line_2: { type: string } sub_district: { type: string } district: { type: string } province: { type: string } country: { type: string, description: "ISO 3166-1 alpha-2, e.g. TH", minLength: 2, maxLength: 2, } zip_code: { type: string, maxLength: 10 } OTPOption: type: object properties: expired_time: { type: integer, format: int64, description: Validity window in seconds., default: 120, } code_type: { type: string, enum: [auto, custom], default: auto } custom_code: { type: string, description: Honored only when code_type is custom. } ref_code: { type: string, description: Caller-supplied correlation id echoed in reports., } code_length: { type: integer, format: int64, default: 6 } MessageItem: type: object required: [sender_name, operator, message] properties: sender_name: { type: string, description: Pre-approved sender name. } operator: { type: string, description: "Carrier id, e.g. ais, dtac, true_corp.", } message: { type: string, description: "Body. Supports {{first_name}} style templating.", } phone_number: { type: string, description: "E.164, required when contact_book_uuid is empty.", } contact_book_uuid: { type: string } transaction_uuid: { type: string, description: UUIDv7 correlation id for DR webhooks and tracking., } tracking_uuid: { type: string } first_name: { type: string } middle_name: { type: string } last_name: { type: string } email: { type: string, format: email } address: { $ref: "#/components/schemas/Address" } birthday: { type: string, description: "YYYY-MM-DD" } digest: { type: string, description: "Scheduled send time, ISO 8601." } meta_data: { type: object, additionalProperties: { type: string } } tags: { type: array, items: { type: string } } timeout_seconds: { type: integer, format: int64 } OTPItem: allOf: - $ref: "#/components/schemas/MessageItem" - type: object properties: options: type: object properties: otp: { $ref: "#/components/schemas/OTPOption" } SendMessageRequest: type: object required: [data] properties: check_spam_msg_mode: type: string enum: [not_check, single, bulk] default: not_check description: Server-side spam detection mode. data: type: array minItems: 1 items: { $ref: "#/components/schemas/MessageItem" } SendOTPRequest: type: object required: [data] properties: check_spam_msg_mode: { type: string, enum: [not_check, single, bulk], default: not_check } data: type: array minItems: 1 items: { $ref: "#/components/schemas/OTPItem" } CancelScheduledRequest: type: object properties: transaction_uuids: { type: array, items: { type: string } } batch_uuids: { type: array, items: { type: string } } SendMessageResponse: type: object properties: check_spam_msg_mode: { type: string } input_message: {} send_messages: type: array items: { $ref: "#/components/schemas/SendMessageItem" } SendMessageItem: type: object properties: sender_name: { type: string } operator: { type: string } message: { type: string } phone_number: { type: string } report: { $ref: "#/components/schemas/SendMessageReport" } SendMessageReport: type: object properties: transaction_uuid: { type: string } tracing_uuid: { type: string } otp: { $ref: "#/components/schemas/OTPReport" } error: { $ref: "#/components/schemas/Error" } OTPReport: type: object properties: is_enabled: { type: boolean } code_type: { type: string } code: { type: string } ref_code: { type: string } expired_time: { type: string } app_id: { type: string } VerifyOTPRequest: type: object required: [phone_number, code, ref_code, app_id] properties: phone_number: { type: string } code: { type: string } ref_code: { type: string } app_id: { type: string } VerifyOTPResponse: type: object properties: status: { type: string } is_valid: { type: boolean } message: { type: string } expired_time: { type: string } CreateSenderNameRequest: type: object required: [sender_name, usage_type, ai_check_valid_sender_mode] properties: sender_name: { type: string } usage_type: { type: string, enum: [General, Transactional, Promotional, OTP] } description: { type: string } ai_check_valid_sender_mode: { type: string, enum: [not_check, check] } UpdateSenderNameStatusRequest: type: object required: [operator_updates] properties: operator_updates: type: object additionalProperties: { type: string, enum: [approve, reject, pending] } SenderName: type: object properties: uuid: { type: string } sender_name: { type: string } usage_type: { type: string } description: { type: string } approved_operators: { type: array, items: { type: string } } created_at: { type: string } updated_at: { type: string } ListSenderNamesResponse: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/SenderName" } } total: { type: integer, format: int64 } page: { type: integer } page_size: { type: integer } total_pages: { type: integer } VoiceOTPOptions: type: object properties: expired_time: { type: integer, format: int64 } code_type: { type: string, enum: [auto, custom] } custom_code: { type: string } ref_code: { type: string } code_length: { type: integer, format: int64 } VoiceOTPRequest: type: object required: [phone_number] properties: phone_number: { type: string } language: { type: string } otp: { $ref: "#/components/schemas/VoiceOTPOptions" } max_retries: { type: integer } retry_interval_secs: { type: integer } prefix_message: { type: string } caller_id: { type: string } tracking_uuid: { type: string } VoiceOTPResponse: type: object properties: session_uuid: { type: string } ref_code: { type: string } code: { type: string } expired_time: { type: string } status: { type: string } VerifyVoiceOTPRequest: type: object required: [phone_number, code, ref_code] properties: phone_number: { type: string } code: { type: string } ref_code: { type: string } VerifyVoiceOTPResponse: type: object properties: is_valid: { type: boolean } status: { type: string } message: { type: string } VoiceBroadcastRequest: type: object required: [script] properties: phone_numbers: { type: array, items: { type: string } } contact_book_uuid: { type: string } script: { type: string } language: { type: string } voice: { type: string } caller_id: { type: string } max_concurrency: { type: integer } schedule: { type: string } VoiceBroadcastResponse: type: object properties: batch_uuid: { type: string } total_targets: { type: integer } queued_count: { type: integer } rejected_count: { type: integer } status: { type: string } DialRequest: type: object required: [agent_id, customer_phone] properties: agent_id: { type: string } customer_phone: { type: string } caller_id: { type: string } record_call: { type: boolean } whisper_message: { type: string } tracking_uuid: { type: string } DialResponse: type: object properties: session_uuid: { type: string } room_name: { type: string } agent_token: { type: string } status: { type: string } DTMFEvent: type: object properties: digit: { type: string } since_start_ms: { type: integer, format: int64 } VoiceSession: type: object properties: uuid: { type: string } tenant_id: { type: string } app_name: { type: string } type: { type: string } status: { type: string } provider: { type: string } provider_ref: { type: string } from_number: { type: string } to_number: { type: string } agent_id: { type: string } script: { type: string } language: { type: string } voice: { type: string } transcript: { type: string } recording_url: { type: string } duration_secs: { type: integer } listened_secs: { type: integer } end_reason: { type: string } cost: { type: number } dtmf_log: { type: array, items: { $ref: "#/components/schemas/DTMFEvent" } } ListSessionsResponse: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/VoiceSession" } } total: { type: integer, format: int64 } limit: { type: integer } offset: { type: integer } IVRMenuOption: type: object properties: action: { type: string } script: { type: string } agent_id: { type: string } repeat: { type: boolean } IVRConfig: type: object required: [did, greeting] properties: uuid: { type: string } tenant_id: { type: string } did: { type: string } greeting: { type: string } language: { type: string } voice: { type: string } no_input_timeout_secs: { type: integer } no_input_action: { type: string } ai_agent_name: { type: string } menu_options: type: object additionalProperties: { $ref: "#/components/schemas/IVRMenuOption" } ListIVRConfigsResponse: type: object properties: data: { type: array, items: { $ref: "#/components/schemas/IVRConfig" } } total: { type: integer, format: int64 } page: { type: integer } page_size: { type: integer } total_pages: { type: integer } DialCallRequest: type: object additionalProperties: false required: [destination] properties: destination: { type: string, description: "The number to call, in E.164." } caller_id: { type: string, description: "One of your numbers to show the callee." } caller_identity_id: { type: string, description: "A saved caller identity instead of caller_id." } caller_extension_uuid: { type: string, description: "Call as this extension's number." } trunk: { type: string, description: "Force a specific trunk." } organization_id: { type: string, description: "Only with a user token; an API key already names its organization." } mode: { type: string, enum: [queue, instant], description: "queue (default) waits for a free line; instant dials now or answers 429." } TTSCallRequest: type: object additionalProperties: false required: [destination, tts] properties: destination: { type: string, description: "The number to call, in E.164." } caller_id: { type: string, description: "One of your numbers to show the callee." } caller_identity_id: { type: string, description: "A saved caller identity instead of caller_id." } caller_extension_uuid: { type: string, description: "Call as this extension's number." } trunk: { type: string, description: "Force a specific trunk." } organization_id: { type: string, description: "Only with a user token; an API key already names its organization." } mode: { type: string, enum: [queue, instant], description: "queue (default) waits for a free line; instant dials now or answers 429." } tts: { $ref: "#/components/schemas/Speech" } AgentCallRequest: type: object additionalProperties: false required: [destination, agent] properties: destination: { type: string, description: "The number to call, in E.164." } caller_id: { type: string, description: "One of your numbers to show the callee." } caller_identity_id: { type: string, description: "A saved caller identity instead of caller_id." } caller_extension_uuid: { type: string, description: "Call as this extension's number." } trunk: { type: string, description: "Force a specific trunk." } organization_id: { type: string, description: "Only with a user token; an API key already names its organization." } agent: type: object additionalProperties: false required: [id] description: A saved agent; the other fields override it for this call. properties: id: { type: string } config_mode: { type: string, enum: [published, draft] } system_prompt: { type: string } voice: { type: string } language: { type: string } gather: type: object additionalProperties: false required: [parameters] properties: parameters: { type: object, description: "JSON schema of the answers; x-parse per field sets the format (text, integer, number, boolean, date, phone, email)." } callback_url: { type: string, format: uri } callback_secret: { type: string } SurveyCallRequest: type: object additionalProperties: false required: [destination, survey] properties: destination: { type: string, description: "The number to call, in E.164." } caller_id: { type: string, description: "One of your numbers to show the callee." } caller_identity_id: { type: string, description: "A saved caller identity instead of caller_id." } caller_extension_uuid: { type: string, description: "Call as this extension's number." } trunk: { type: string, description: "Force a specific trunk." } organization_id: { type: string, description: "Only with a user token; an API key already names its organization." } survey: { $ref: "#/components/schemas/Survey" } FlowCallRequest: type: object additionalProperties: false required: [destination, flow_uuid] properties: destination: { type: string, description: "The number to call, in E.164." } caller_id: { type: string, description: "One of your numbers to show the callee." } caller_identity_id: { type: string, description: "A saved caller identity instead of caller_id." } caller_extension_uuid: { type: string, description: "Call as this extension's number." } trunk: { type: string, description: "Force a specific trunk." } organization_id: { type: string, description: "Only with a user token; an API key already names its organization." } flow_uuid: { type: string } BatchCallRequest: type: object additionalProperties: false required: [destinations] properties: destinations: { type: array, items: { type: string }, minItems: 1 } caller_id: { type: string } caller_identity_id: { type: string } caller_extension_uuid: { type: string } trunk: { type: string } organization_id: { type: string } mode: { type: string, enum: [queue, instant], description: "queue (default) waits for a free line; instant dials now or answers 429." } tts: { $ref: "#/components/schemas/Speech" } Speech: type: object additionalProperties: false required: [text, voice_id] properties: text: { type: string } voice_id: { type: string } provider: { type: string, description: "Empty: chosen from voice_id." } language: { type: string } repeat: { type: integer, description: "Times the message plays; 0 means once." } Survey: type: object additionalProperties: false description: | mode "dtmf" uses questions, survey_id, closing, retry_prompt and tts. mode "agent" (default) uses parameters, language and voice. properties: mode: { type: string, enum: [agent, dtmf] } survey_id: { type: string, pattern: "^[a-z][a-z0-9_]{0,63}$", description: "Groups answers in survey reports; each is stored as .. Default telephony_survey." } parameters: { type: object, description: "Agent mode: JSON schema of the answers." } questions: type: array maxItems: 20 items: { $ref: "#/components/schemas/SurveyQuestion" } greeting: { type: string } closing: { type: string } retry_prompt: { type: string, description: "Said before a question is asked again after a key out of range; {min} and {max} are filled in." } language: { type: string } voice: { type: string } tts: type: object additionalProperties: false properties: provider: { type: string } voice_id: { type: string } language: { type: string } instructions: { type: string } callback_url: { type: string, format: uri } callback_secret: { type: string } SurveyQuestion: type: object additionalProperties: false required: [key, prompt, min, max] properties: key: { type: string, pattern: "^[a-z][a-z0-9_]{0,63}$" } prompt: { type: string } min: { type: integer, minimum: 0, maximum: 9 } max: { type: integer, minimum: 0, maximum: 9 } required: { type: boolean, default: true, description: "An unanswered required question ends the call." } PlacedCall: type: object properties: call_uuid: { type: string } trunk: { type: string } destination: { type: string } status: { type: string, description: "queued, dialing or ringing." } room: { type: string } session_uuid: { type: string } tts: type: object properties: provider: { type: string } voice_id: { type: string } characters: { type: integer } repeat: { type: integer } cached: { type: boolean } gather: type: object description: Present when the call collects answers. properties: gather_id: { type: string } callback_secret: { type: string, description: "Verifies the callback signature." } PlaceCallResponse: type: object properties: kind: { type: string, enum: [pstn] } call: { $ref: "#/components/schemas/PlacedCall" } CallStatus: type: object properties: call_uuid: { type: string } room: { type: string } destination: { type: string } direction: { type: string } status: { type: string, description: "pending, ringing, in_progress, completed, no_answer, failed or canceled." } handled_by_ai: { type: boolean } started_at: { type: string, format: date-time } duration_secs: { type: integer }