# MessageEagle — Full Documentation > Developer documentation for the MessageEagle messaging platform — SMS, Voice, and Verify APIs. --- # MessageEagle Source: /
# MessageEagle Every language lives in its own folder. If you are not redirected: [English](/en/) · [ไทย](/th/) · [日本語](/ja/) · [한국어](/ko/) · [简体中文](/zh-Hans/) · [繁體中文](/zh-Hant/)
--- # en/index Source: /en --- # API Reference Source: /en/api-reference --- # Messaging Best Practices Source: /en/best-practices/best-practices # Messaging Best Practices Getting a message delivered is more than calling an endpoint. This page collects the practices that move you from "it sent" to "it consistently lands, on the right carrier, with the right content." ::: tip Hand this page to an AI Use **Copy page** (top right) to copy this guide as Markdown, or **Open in Claude / ChatGPT / Gemini** to ask questions about it directly. ::: ## Choose the right send category Not every message belongs on the same route. - **Transactional** (`/sms/send_messages/message`) — receipts, codes, alerts. Latency-sensitive; keep this queue clean. - **Broadcast** (`/sms/broadcast`) — promotions and announcements. Requires `ME_SMS_PRODUCER_ENABLED` on the deployment. Separately rate-limited so it never blocks transactional traffic. - **OTP** — send the code with `/sms/send_messages/message`, then verify it at `/otp/verify`. There is no OTP send route; expiry built in. ::: warning Don't send promos through the transactional route A large promo blast on `/message` competes with time-critical OTPs and receipts. Always use `/broadcast` for marketing. ::: ## Template instead of concatenate Build message bodies with placeholders, not string concatenation. The platform resolves {{first_name}} and {{meta_data.*}} per recipient, so one payload personalizes a whole batch. ```json { "message": "Hi {{first_name}}, your balance is {{meta_data.balance}}.", "first_name": "Mali", "meta_data": { "balance": "1,250" } } ``` ## Always attach a `transaction_uuid` A per-message UUIDv7 is the thread that ties together: 1. The send call, 2. The [delivery-report webhook](/en/best-practices/webhooks), 3. Any later tracking lookups. Without it, you can't correlate a status callback back to the original message. ## Route by carrier deliberately `operator` selects the carrier (`ais`, `dtac`, `true_corp`). Make sure the `sender_name` is [approved](/en/capabilities/sender-names) for that carrier — an unapproved pairing fails **that item** while the rest of the batch sends. ## Use spam checking for user-generated content If message bodies come from your users, set `check_spam_msg_mode`: - `single` — drop only the offending items. - `bulk` — refuse the whole batch unless every item passes. ## Verify OTPs defensively - Keep `expired_time` short (3–5 minutes) and surface a clear resend path. - Store the `ref_code`; you need it to verify. - Never log full OTP codes alongside phone numbers. ## Pre-send checklist - [ ] `sender_name` approved for the target `operator` - [ ] `phone_number` in E.164 (`+66…`) - [ ] `transaction_uuid` set for traceability - [ ] Correct send category (`message` vs `broadcast` vs `otp`) - [ ] Spam mode chosen for user-generated bodies - [ ] A [webhook](/en/best-practices/webhooks) is receiving delivery reports ## Next steps - [Rate Limits & Quotas](/en/best-practices/rate-limits) - [Webhooks & Delivery Reports](/en/best-practices/webhooks) - [API Reference](/en/api-reference) --- # Rate Limits & Quotas Source: /en/best-practices/rate-limits # Rate Limits & Quotas Sends are throttled per tenant and per queue. Transactional and broadcast traffic are metered **separately**, so a broadcast blast cannot exhaust your transactional budget. ## Design for throttling - **Batch within a single request.** Each send endpoint accepts an array in `data` (currently up to 100 items) — prefer one call with many items over many calls with one item. - **Back off on `429`.** Treat a rate-limit response as transient: retry with exponential backoff and jitter, capped at ~5 attempts. - **Make retries idempotent.** Reuse the same `transaction_uuid` on retry so a duplicate never sends twice. ```ts async function sendWithRetry(body: unknown, attempt = 0): Promise { const res = await fetch(`${ME_BASE}/sms/send_messages/message`, { method: "POST", headers: { "X-Token": ME_TOKEN, "X-TenantID": ME_TENANT, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (res.status === 429 && attempt < 5) { const wait = Math.min(2 ** attempt * 250, 8000) + Math.random() * 250; await new Promise((r) => setTimeout(r, wait)); return sendWithRetry(body, attempt + 1); } return res; } ``` ::: warning Don't tight-loop retries Retrying immediately on `429` makes throttling worse. Always wait, and always add jitter so many clients don't retry in lockstep. ::: ## Scheduling smooths spikes For large sends with a future target time, set `digest` and let the platform pace delivery rather than firing everything at once. Cancel before dispatch with the broadcast batch status route, `GET /sms/broadcast/{batchId}`. --- # Webhooks & Delivery Reports Source: /en/best-practices/webhooks # Webhooks & Delivery Reports A successful send means "accepted for delivery", not "delivered". Delivery reports (DRs) arrive asynchronously as webhooks keyed by the `transaction_uuid` you set on the message. ## Receive a delivery report Point a public HTTPS endpoint at MessageEagle and handle the callback: ```ts app.post("/webhooks/messageeagle", (req, res) => { const { transaction_uuid, status } = req.body; // 1. Acknowledge fast — return 2xx before doing slow work. res.sendStatus(200); // 2. Look up the original message by transaction_uuid and update its status. queue.enqueue({ transaction_uuid, status }); }); ``` ## Build for the hard case Webhooks are **at-least-once**. Design every handler accordingly. - **Idempotent updates.** Key on `transaction_uuid`; applying the same DR twice must be a no-op. Prefer an UPSERT over an INSERT. - **Acknowledge first, process later.** Return `2xx` immediately, then do the real work on a queue. Slow handlers cause redeliveries. - **Tolerate out-of-order.** A `delivered` can arrive before a `sent`. Use a status precedence, not last-write-wins. ::: warning Verify the source Validate the request signature (or a shared secret) before trusting a payload. Never act on a webhook body you can't authenticate. ::: ## Status model | Status | Meaning | | ----------- | ------------------------------------------------------- | | `queued` | Accepted, awaiting dispatch. | | `sent` | Handed to the carrier. | | `delivered` | Confirmed on the handset. | | `failed` | Carrier rejected or could not deliver. | | `expired` | TTL elapsed before delivery (e.g. unreachable handset). | ## Voice completion Voice sessions emit completion callbacks too — recordings, transcripts, DTMF logs, and cost land on the [`VoiceSession`](/en/capabilities/voice). Poll `GET /organization/{organizationId}/webhook-deliveries` as a fallback if a callback is missed — see [Webhook Deliveries](/en/capabilities/webhook-deliveries). --- # Voice Agents Source: /en/capabilities/agents # Voice Agents Two things live here: the **agent configurations** that decide how an agent behaves, and the **catalog** of speech providers and voices it can speak with. | Action | Route | | ------------------------ | ---------------------------------------- | | List configurations | `GET /agents/configs` | | Get one | `GET /agents/configs/{documentId}` | | Create | `POST /agents/configs` | | Replace | `PUT /agents/configs/{documentId}` | | Delete | `DELETE /agents/configs/{documentId}` | | List providers | `GET /voice/providers` | | List a provider's voices | `GET /voice/providers/{provider}/voices` | ## Configurations A configuration is a document — the studio adds fields to it as agents gain capabilities, so treat unknown keys as pass-through rather than dropping them. ```bash curl "$ME_BASE/agents/configs" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` ## Provider and voice catalog Which providers are available is a **per-deployment** question, not a constant: availability follows the vendor keys configured for your environment. Read the catalog at runtime rather than hard-coding a provider list. ```bash curl "$ME_BASE/voice/providers/elevenlabs/voices" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` ```json { "data": [ { "id": "voice_abc", "name": "Mali", "gender": "female", "language": "th-TH", "preview": true } ] } ``` | Field | Notes | | --------- | -------------------------------------------------------- | | `id` | Pass this as the agent's voice id. | | `preview` | Whether **this** voice can be rendered to a sample here. | ::: tip Honour `preview` `preview` is resolved per request because it turns on and off with a vendor key. When it is `false`, disable the play button — never substitute a different voice for the sample, or the picker plays something the agent will not speak with. ::: ## Go SDK ```go configs, err := c.Agents.ListConfigs(ctx) voices, err := c.Agents.ListVoices(ctx, "elevenlabs") ``` --- # Live Call Control Source: /en/capabilities/calls # Live Call Control These endpoints act on a call that is **already in progress**. They are addressed by LiveKit room name — not by session id — because the room is what exists while people are still talking. | Action | Route | Use it for | | --------------- | -------------------------------------------------------- | ------------------------------------------ | | **Hold** | `POST /voice/calls/{roomName}/hold` | Put the caller on hold, or resume. | | **Mode** | `POST /voice/calls/{roomName}/mode` | Switch the supervisor listening mode. | | **Hangup** | `POST /voice/calls/{roomName}/hangup` | End the call and seal its session. | | **Lock** | `POST /voice/calls/{roomName}/lock` | Stop further participants joining. | | **Kick** | `DELETE /voice/calls/{roomName}/participants/{identity}` | Remove one participant. | | **Force close** | `POST /voice/calls/{uuid}/force-close` | Seal a session whose room is already gone. | ::: danger An API key can control live calls A key carries its **owner's** permissions on these routes. Anyone holding it can hang up or take over a call in progress, so treat it like a password: keep it server-side and rotate it if it leaks. ::: ## Hold and resume Hold does two things: it 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. ```bash curl -X POST "$ME_BASE/voice/calls/$ROOM/hold" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{"muted": true, "holder_identity": "agent-42"}' ``` ```json { "status": "ok", "held": true } ``` Send `"muted": false` on the same route to resume. | Field | Type | Notes | | ---------------------- | ------ | ---------------------------------------------- | | `muted` | bool | `true` holds, `false` resumes. | | `participant_identity` | string | Optional. Narrows the hold to one participant. | | `track_sid` | string | Optional. Narrows the hold to one track. | | `holder_identity` | string | Optional. Recorded in the call log. | ## Supervisor mode ```bash curl -X POST "$ME_BASE/voice/calls/$ROOM/mode" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{"mode": "whisper", "identity": "supervisor-7"}' ``` ```json { "mode": "whisper" } ``` Pass `exclude_identities` to leave specific participants untouched. ## Hangup vs force close `hangup` ends a live call: it deletes the room and seals the session. Use it while people are connected. `force-close` seals a **session by its uuid** and exists for the case where the room is already gone but the session never closed. It is a recovery path, not the normal way to end a call. ```bash curl -X POST "$ME_BASE/voice/calls/$ROOM/hangup" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" -d '{}' ``` ## Go SDK ```go out, err := c.Calls.Hold(ctx, roomName, &sdk.HoldRequest{Muted: true}) _, err = c.Calls.SetMode(ctx, roomName, &sdk.ModeRequest{Mode: "whisper"}) _, err = c.Calls.Hangup(ctx, roomName, nil) ``` --- # Desk Phone Provisioning Source: /en/capabilities/desk-phone-provisioning # Desk Phone Provisioning A desk phone cannot log in. It boots, asks the network where its configuration lives, fetches a file, and registers with whatever it finds there. Everything on this page exists to make that one fetch land on the right file. There are two ways to get a fleet online. They differ only in **when** you tell the platform which extension belongs to which handset. | | Pre-seed | Discover then assign | |---|---|---| | You know the MAC addresses in advance | yes | no | | Work per phone after unboxing | none | pick an extension, then re-provision | | Phone registers on first boot | yes | no | Both use the same site URL, and you can mix them — a phone that arrives without a pre-seeded MAC simply shows up in the discovered list. ## 1. Create the site enrollment URL In **Telephony → Extensions → Add phones → Auto discover**, pick the site and create a token. You get one URL for the whole branch: ```text https:///v1/voice/provision/e// ``` Note the trailing `/`. It is a directory, not a file — without it the phone asks for `/.cfg`, outside its provisioning directory, and every fetch after the first returns 404. ::: tip The token is not a credential This URL never returns SIP passwords. A guessed token yields an empty device row and a file that says only "fetch from here next". Real credentials are served only from a device's own directory, and only once an extension is assigned. You can print this URL on a wall. ::: Two optional limits are worth setting: a **device cap** for the site, which bounds junk rows if the token leaks, and an **expiry**. Revoking a token does not disturb phones already enrolled. ## 2. Point DHCP option 66 at it ### On a 3BB router Open `http://192.168.1.1` (some models use `192.168.0.1`) and sign in as administrator, then find the DHCP section — typically: ```text Network → LAN → DHCP Server → DHCP Settings ``` Menu names vary by hardware. Look for any of: **DHCP Option**, **Option 66**, **TFTP Server Name**, **Boot Server**, **Provisioning Server**, or **Vendor Specific DHCP Option**. | Field | Value | |---|---| | Option | 66 | | Type | String / Text | | Value | the site URL from step 1 | | Scope | the LAN DHCP scope the phones sit in | No quotation marks, and keep the trailing `/`. Save, then restart the DHCP server or reboot the router once. ::: warning Many 3BB routers accept only a TFTP host Firmware supplied by the ISP is often locked down, and some builds accept option 66 only as a bare hostname or IP — a full `https://` URL is silently rejected or truncated. If the phone's **Server URL** is empty or wrong after a reboot, this is the most likely cause. Use one of the fallbacks below rather than fighting the firmware. ::: ### When the router has no option 66 1. **Use your own router.** Put the 3BB unit in bridge mode and run DHCP yourself. 2. **Run a separate DHCP server** — MikroTik, OpenWrt, pfSense, Windows DHCP, dnsmasq, or a UniFi gateway. For dnsmasq: ```ini dhcp-option=66,https:///v1/voice/provision/e// ``` 3. **Set it on the phone.** On a Yealink: **Settings → Auto Provision → Server URL**. This works, but it is one visit per handset — the thing option 66 exists to avoid. ## 3. Choose how extensions get assigned ### Pre-seed (zero-touch) In **Add phones → Import from CSV**, upload a file with the MAC printed on each box: ```csv name,extension,mac,vendor,model Reception,1001,00:15:65:AA:BB:CC,yealink,yealink-t31w Sales 1,1002,00:15:65:AA:BB:CD,yealink,yealink-t31w Conference,1003,,poly,poly-vvx350 ``` - `name`, `vendor`, `model` are required; `extension` must be an existing extension number. - `mac` is optional per row — the third line above has none, so that phone falls back to discovery. - Any separator style works (`00:15:65:AA:BB:CC`, `001565aabbcc`, `00-15-65-aa-bb-cc`, `0015.65aa.bbcc`). The preview shows the form that will actually be stored. - A MAC already registered to another device is rejected at upload rather than at boot. Plug the phones in. Each one asks for its own `.cfg`, is recognised, and registers. ### Discover then assign Plug the phones in first. Each unknown handset creates a row in the discovered list within a few minutes, showing its MAC and a short activation code. Then either pick an extension for each row and press **Assign**, or use **Activate by code** to claim one phone by the code beside it. ::: warning Assigning does not reach the phone by itself The handset applies its new extension on its next auto-provision check, which can be many hours away. Reboot it, or press **Autoprovision Now** on the phone, to apply it immediately. ::: ## 4. Confirm it worked On the phone, **Status** shows its MAC and IP; **Settings → Auto Provision** shows the Server URL it received from DHCP. After provisioning, **Account → Register Status** should read `Registered`, and **Status → System Log** carries the fetch attempts. In the dashboard, **Telephony → Extensions → Activity** logs every provisioning fetch and every registration lookup, including the failures — a config served with no extension bound, a MAC refused because it belongs to another organization, or a registration for an extension no device owns. ::: tip A phone showing `localhost` is pointing at itself If the Server URL on the handset reads `http://localhost:3003` or `127.0.0.1`, the phone is resolving that to its own address and will never reach the platform. The provisioning host must be a name the phone can resolve on its own network. ::: ## Reference Site enrollment tokens are managed through the organization-scoped API. Paths are relative to the API base shown in the [API Reference](/en/api-reference); `organizationId` is the same value sent as `X-TenantID`. | Method | Path | |---|---| | `GET` | `/organization/{organizationId}/voice/enrollment-tokens` | | `POST` | `/organization/{organizationId}/voice/enrollment-tokens` | | `DELETE` | `/organization/{organizationId}/voice/enrollment-tokens/{uuid}` | `POST` takes `org_node_uuid` and optionally `label`, `max_devices` (`0` = unlimited), and `expires_at`. The phone-facing route the option 66 URL points at is **unauthenticated by design** — a booting handset has no credentials to present — and is therefore not published as a callable operation here. It is rate-limited per client address, and it refuses a MAC that belongs to another organization with the same `404` an unknown token receives, so a scanner cannot tell the two apart. --- # Place Calls Source: /en/capabilities/place-calls # Place Calls Each kind of call has its own endpoint. Pick the one that matches what should happen when the callee answers: | Kind | Route | When the callee answers | Reply | | ---------- | -------------------------------- | --------------------------------------------- | --------------- | | **Dial** | `POST /telephony/call/dial` | Nothing plays — use the call to park or transfer it. | `202` queued | | **TTS** | `POST /telephony/call/tts` | A message is spoken, then the call hangs up. | `202` queued | | **Agent** | `POST /telephony/call/agent` | An AI agent talks with them. | `201` ringing | | **Survey** | `POST /telephony/call/survey` | Questions are asked and the answers returned. | `202` or `201` | | **Flow** | `POST /telephony/call/flow` | Not available yet — the call rings, but no flow runs. | `201` ringing | | **Batch** | `POST /telephony/call/batch` | One TTS or dial call per number. | `202` queued | Then track any of them with `GET /telephony/call/{call_uuid}`. Your API key needs the **`voice.originate`** permission. A call to an internal extension (`POST /telephony/call/extension`) needs a signed-in user instead — an API key has no handset to ring. ::: warning Each endpoint accepts only its own fields A field that belongs to another kind of call is refused with `400` and names the right endpoint — for example `survey belongs to POST /telephony/call/survey`. The older `POST /telephony/call` still works for dial, TTS, agent, flow and extension calls, but is deprecated and cannot run a survey. ::: ## Who is called, and from where Every call to a number takes the same target fields: | Field | Type | Notes | | ----------------------- | ------ | --------------------------------------------------------------- | | `destination` | string | Required. The number to call, in E.164 (`+66812345678`). | | `caller_id` | string | Optional. One of your numbers to show the callee. | | `caller_identity_id` | string | Optional. A saved caller identity instead of `caller_id`. | | `caller_extension_uuid` | string | Optional. Call as this extension's number. | | `trunk` | string | Optional. Force a specific trunk. | | `organization_id` | string | Only with a user token. An API key already names its organization. | Dial, TTS and batch also take `mode`: `"queue"` (default) waits for a free line; `"instant"` dials now or answers `429` when every line is busy. ## Retry safely with `Idempotency-Key` Send an `Idempotency-Key` header on every call you place. A retry with the same key returns the first call instead of dialing the callee a second time. The official SDKs send one for you and reuse it on their own retries. ```bash curl -X POST "$ME_BASE/telephony/call/tts" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Idempotency-Key: order-1042-reminder" \ -H "Content-Type: application/json" \ -d '{ "destination": "+66812345678", "tts": { "text": "Your order 1042 is out for delivery.", "voice_id": "eve" } }' ``` ```json { "kind": "pstn", "call": { "call_uuid": "5d2c…", "destination": "+66812345678", "status": "queued", "tts": { "provider": "xai", "voice_id": "eve", "characters": 36, "repeat": 0, "cached": false } } } ``` `call.tts` reports how the message was rendered. `tts` takes `text`, `voice_id`, and optionally `provider`, `language` and `repeat` (how many times it plays; `0` means once). ## Connect an AI agent ```json { "destination": "+66812345678", "agent": { "id": "agent_support_th" }, "gather": { "parameters": { "type": "object", "properties": { "order_id": { "type": "string" } }, "required": ["order_id"] }, "callback_url": "https://example.com/hooks/order" } } ``` `agent.id` names a saved agent (add `config_mode: "draft"` to use its draft). `system_prompt`, `voice` and `language` override that agent for this call only. `gather` is optional: the agent asks for the fields in `parameters` and returns them — see [Getting the answers](#getting-the-answers). ## Run a survey A survey calls the number, asks your questions, and returns the answers. It runs in one of two modes. ### Keypad (`"mode": "dtmf"`) A voice reads each question and the callee answers by pressing one key. ```json { "destination": "+66812345678", "survey": { "mode": "dtmf", "survey_id": "csat_q3", "greeting": "Thanks for calling us today.", "questions": [ { "key": "score", "prompt": "How satisfied were you? Press 1 to 5.", "min": 1, "max": 5 }, { "key": "would_return", "prompt": "Would you use us again? Press 1 for no, 2 for yes.", "min": 1, "max": 2, "required": false } ], "retry_prompt": "Please press a number from {min} to {max}.", "closing": "Thank you. Goodbye.", "tts": { "provider": "xai", "voice_id": "eve", "language": "en" }, "callback_url": "https://example.com/hooks/survey" } } ``` | Field | Notes | | -------------- | --------------------------------------------------------------------------------------- | | `questions` | Required. Up to 20, asked in order. Each takes one key between `min` and `max` (0–9). | | `key` | Lowercase letters, digits and `_`, starting with a letter. Names the answer. | | `required` | Default `true`. An unanswered required question ends the call; an optional one is skipped. | | `retry_prompt` | Said before a question is asked again after a key outside its range. `{min}` and `{max}` are filled in. Without it, the question is simply repeated. The callee gets 3 tries. | | `survey_id` | Groups the answers in your survey reports. Each answer is stored as `.` — `csat_q3.score` — so every question keeps its own scale. Default `telephony_survey`. | | `tts` | The voice for every prompt: `provider`, `voice_id`, `language`, `instructions`. An unavailable provider is refused with `400` before the call is queued. | A keypad survey is queued: the reply is `202` with `status: "queued"`. ::: tip Put the better answer on the higher key Survey reports count a score as satisfied when it is in the top two of its scale. For a yes/no question, make **yes** the higher key (`1` no, `2` yes) so the report reads the way you expect. ::: ### Spoken (`"mode": "agent"`, the default) An AI agent asks for the fields in `parameters` by voice and hangs up once every required answer is valid. It rings now: the reply is `201`. ```json { "destination": "+66812345678", "survey": { "greeting": "Hi, this is a short survey from Acme.", "parameters": { "type": "object", "properties": { "rating": { "type": "integer", "x-parse": "integer" }, "comment": { "type": "string" } }, "required": ["rating"] }, "language": "th", "callback_url": "https://example.com/hooks/survey" } } ``` `x-parse` sets the format the agent returns for a field: `text`, `integer`, `number`, `boolean`, `date`, `phone` or `email`. Keypad-only fields (`questions`, `closing`, `retry_prompt`, `survey_id`, `tts`) are refused here. ## Getting the answers Answers from a gather or a survey reach you two ways, with the same body: - **Webhook** — the `call.ai_gather.ended` event on your workspace webhooks, sent when an answered call ends, with `status` `valid` or `incomplete`. - **`callback_url`** — only when every required answer is `valid`. It is signed: `X-webhook-signature` is the base64 HMAC-SHA256 of the raw body, keyed with `call.gather.callback_secret` from the place reply (or the `callback_secret` you sent). ```json { "event": "call.ai_gather.ended", "id": "evt_…", "created_at": "2026-09-25T09:30:12Z", "workspace_id": "org_…", "gather_id": "…", "session_uuid": "…", "status": "valid", "result": { "score": 4, "would_return": 2 } } ``` Match `gather_id` against `call.gather.gather_id` from the place reply. ## Call many numbers ```json { "destinations": ["+66811111111", "+66822222222"], "tts": { "text": "The office is closed today.", "voice_id": "eve" } } ``` ```json { "queued": 2, "total": 2, "calls": [{ "destination": "+66811111111", "call": { "call_uuid": "…", "status": "queued" } }] } ``` A number that could not be queued has an `error` in its item; the rest still go out. ## Call status ```bash curl "$ME_BASE/telephony/call/$CALL_UUID" \ -H "API-Key: $ME_API_KEY" -H "X-TenantID: $ME_TENANT" ``` ```json { "call_uuid": "5d2c…", "room": "…", "destination": "+66812345678", "status": "completed", "handled_by_ai": false, "duration_secs": 42 } ``` `status` moves through `pending`, `ringing` and `in_progress`, and ends as `completed`, `no_answer`, `failed` or `canceled`. ## SDKs ```go res, err := c.Telephony.Survey(ctx, &sdk.SurveyCallRequest{ DialTarget: sdk.DialTarget{Destination: "+66812345678"}, Survey: &sdk.Survey{ Mode: sdk.SurveyModeDTMF, SurveyID: "csat_q3", Questions: []sdk.SurveyQuestion{{Key: "score", Prompt: "Press 1 to 5.", Min: 1, Max: 5}}, }, }) ``` ```ts const res = await me.telephony.survey({ destination: "+66812345678", survey: { mode: "dtmf", survey_id: "csat_q3", questions: [{ key: "score", prompt: "Press 1 to 5.", min: 1, max: 5 }] }, }); ``` Every SDK has the same `telephony` service: `dial`, `tts`, `agent`, `survey`, `flow`, `extension`, `batch` and `status`. --- # Sender Names Source: /en/capabilities/sender-names # Sender Names A sender name is the from-address your recipients see. Carriers require it to be registered and approved before it can send, which is why registering one submits a KYC dossier rather than just a string. All routes are org-scoped, and they authenticate differently from the sending APIs: they take a signed-in user's token, not an API key. An `API-Key` here is rejected with `401` — these routes authorize against your membership of `{organizationId}`, which they read from the path, so there is no `X-TenantID` to send either. | Action | Route | | ------------------ | ----------------------------------------------------------------- | | List | `GET /organization/{organizationId}/sender-names` | | Check availability | `GET /organization/{organizationId}/sender-names/availability` | | Register | `POST /organization/{organizationId}/sender-names` | | Update the dossier | `PUT /organization/{organizationId}/sender-names/{documentId}` | | Delete | `DELETE /organization/{organizationId}/sender-names/{documentId}` | ## Check availability first ```bash curl "$ME_BASE/organization/$ME_TENANT/sender-names/availability?sender_name=MyBrand" \ -H "Authorization: Bearer $ME_ACCESS_TOKEN" ``` ## Register Registration is a KYC submission: the carrier needs to know who is behind the name, not just what it is. ```bash curl -X POST "$ME_BASE/organization/$ME_TENANT/sender-names" \ -H "Authorization: Bearer $ME_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "sender_name": "MyBrand", "usage_type": "Transactional", "business_name": "My Company Co., Ltd.", "contact_person": "Mali Sukjai", "country": "TH", "email": "ops@example.com", "name": "Mali Sukjai", "nationality": "TH", "phone_number": "+66886578553", "purpose": "Order and delivery notifications", "company_registration_certificate": "https://…/cert.pdf", "brand_ownership_proof": "https://…/trademark.pdf" }' ``` ## Updating `PUT` is a **partial patch** of the dossier. Four fields are deliberately not patchable: | Field | Why | | -------------------- | --------------------------------------------- | | `sender_name` | A changed name needs a fresh KYC review. | | `obj_status` | Server-controlled — it is the review outcome. | | `is_active` | Server-controlled. | | `approved_operators` | Set by the carriers that approved the name. | To change the name itself, register a new one. ::: tip Approval is per carrier `approved_operators` lists the carriers that approved this name. Sending with a name that is not approved for the target carrier does not fail the whole request — those recipients are counted in `rejected`. See [SMS Messaging](/en/capabilities/sms). ::: --- # SMS Messaging Source: /en/capabilities/sms # SMS Messaging Two send surfaces, and they are not interchangeable: | Category | Route | Use it for | | ------------- | --------------------------------- | ------------------------------------------- | | **Message** | `POST /sms/send_messages/message` | Transactional sends, one row per recipient. | | **Broadcast** | `POST /sms/broadcast` | The same message to many recipients. | ::: warning Broadcast is a deployment opt-in `/sms/broadcast` is not mounted unless the deployment sets `ME_SMS_PRODUCER_ENABLED`. An environment without it answers `404` — not an error you can act on. Check with your operator before building against it. ::: ## Send a message Each row carries its own sender, operator, recipient, and body. Rows sharing operator + sender name + message are fanned out as **one batch each**. ```json { "check_spam_msg_mode": "not_check", "data": [ { "sender_name": "MyBrand", "operator": "ais", "phone_number": "+66886578553", "message": "Your order has shipped." } ] } ``` - **`sender_name`** must be [approved](/en/capabilities/sender-names) for the carrier. - **`operator`** is the carrier id (`ais`, `dtac`, `true_corp`). - **`phone_number`** is E.164. - **`timeout_seconds`** is accepted for wire compatibility; delivery timeouts are the gateway's policy, so setting it changes nothing. The response is wrapped in `data`: ```json { "data": { "accepted": 1, "rejected": 0, "batches": [ { "batch_id": "018f7b2a-7c3e-7a1b-9c2d-1a2b3c4d5e6f", "accepted": 1, "rejected": 0 } ] } } ``` `accepted` counts recipients queued for the carrier. `rejected` counts those dropped before queueing — a blank or duplicate number, or a publish error. One bad number never fails the whole request. ## Broadcast One message, one operator, one sender, many recipients: ```bash curl -X POST "$ME_BASE/sms/broadcast" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "operator": "ais", "sender_name": "MyBrand", "message": "Sale ends tonight.", "recipients": ["+66886578553", "+66886578554"], "idempotency_key": "campaign-2026-09-12" }' ``` | Field | Notes | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `idempotency_key` | Dedupes retries — a replay with the same key returns the original response instead of re-sending the batch. | | `validity_seconds` | Asks the **carrier** to drop the message if undelivered in this window. Honoured by DTAC and True; AIS has no expiry element, so it is ignored there. | Then poll the batch: ```bash curl "$ME_BASE/sms/broadcast/$BATCH_ID" \ -H "API-Key: $ME_API_KEY" -H "X-TenantID: $ME_TENANT" ``` ```json { "batch_id": "…", "total": 2, "delivered": 1, "sent": 1, "failed": 0, "timeout": 0, "pending": 0 } ``` ## `undeliverable_chars` A batch may come back with `undeliverable_chars` — codepoints the carrier that carried it is **known to destroy**, listed as `"U+1F381 U+1D7EE"`. It exists because the loss is otherwise invisible from the outside: the carrier accepts the message, the surrogate pair is billed as two units, and the handset shows nothing there. If you see it, substitute a character that survives. ::: warning Older SMS endpoints Earlier documentation described `/sms/send_messages/{otp,broadcast,eagleeye,cancel}`. Those routes are **not part of the current API**. OTP verification moved to [Verify / OTP](/en/capabilities/verify); broadcast is `/sms/broadcast` above. ::: --- # Verify / OTP Source: /en/capabilities/verify # Verify / OTP Two surfaces, both authenticated with your API key. **SMS OTP** owns the whole lifecycle — it mints the code, sends it, and verifies it: | Action | Route | | ------------- | -------------------------------- | | Send | `POST /sms/otp` | | Verify | `POST /sms/otp/{otpId}/verify` | | Resend | `POST /sms/otp/{otpId}/resend` | | Read session | `GET /sms/otp/{otpId}` | **Standalone verify** checks a code the platform issued elsewhere — during signup, for example — and is keyed by `ref_code`, not by a session id: | Action | Route | | ------------- | ------------------ | | Verify a code | `POST /otp/verify` | ::: tip Which one do you want? Starting from scratch, use `POST /sms/otp`. Use `/otp/verify` only when something else already issued the code and handed you its `ref_code` — a code you generated and sent over your own SMS cannot be verified there, because the lookup finds nothing. ::: ## Send an OTP ```bash curl -X POST "$ME_BASE/sms/otp" \ -H "API-Key: $ME_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "mode": "auto", "operator": "smart", "sender_name": "YourBrand", "recipient": "+66886578553", "config": { "length": 6, "type": "numeric", "validity_seconds": 300 } }' ``` ```json { "id": "0f3c…", "mode": "auto", "recipient": "+66886578553", "ref_code": "A1B2", "expires_at": "2026-09-19T04:21:00Z", "status": "sent", "message_id": "tx-…" } ``` Keep the `id` — it names the session for verify, resend and read. Show the user `ref_code`. **The code itself is never returned**: only a peppered hash is stored, so nothing can give it back to you. | Field | Notes | | --------------------- | ------------------------------------------------------------------------------------ | | `mode` | `auto` (we mint and verify) or `provided` (you mint; transport only). | | `operator` | `ais`, `dtac`, `true_corp`, or `smart` to let the SMS layer route. | | `sender_name` | A name approved for your organization. See below. | | `recipient` | E.164. | | `message_template` | Must contain `{{otp}}`. Omitted, the default template is used. | | `message_template_id` | A saved template of yours, by id. Mutually exclusive with `message_template`. | | `config` | `auto` only — `length` 4–10, `type`, `validity_seconds` 1–3600. | | `otp` / `ref_code` | `provided` only — your code and your reference. | `sender_name` is the same contract as [SMS](/en/capabilities/sms): an unapproved name is rejected with `403`, not silently substituted. List yours with `GET /organization/{organizationId}/sender-names` — that route takes a user token, not an `API-Key`; see [Sender Names](/en/capabilities/sender-names). That list also carries the platform default, marked `is_platform_default: true` — a name any organization may send under with no approval of its own. It exists only when the deployment configures one, so read it from the list rather than assuming there is one. ### Message templates `{{otp}}` is required — a message without it bills for an SMS the user cannot act on. `{{ref_code}}` is optional. ```jsonc // Inline { "message_template": "{{otp}} is your code. Ref: {{ref_code}}" } // Or by id, from /template-message/organization/{organizationId} { "message_template_id": "8f1c…" } ``` Sending **both** is rejected with `400`. Two sources for one value leave your intent unknowable, and guessing would ship a message you never wrote. A template id is resolved to its **text** before the message is rendered. So editing that template afterwards cannot rewrite a message already sent — or a resend of it, which replays the text as it was at first send. ### Retrying safely Send `Idempotency-Key` and a replay returns the original OTP instead of minting a new one. Naming a template by id and pasting its text count as the *same* request, because the key covers the resolved message. Reusing a key with a genuinely different body is rejected — replaying the first response there would report a code as delivered that never was. ## Verify an SMS OTP ```bash curl -X POST "$ME_BASE/sms/otp/$OTP_ID/verify" \ -H "API-Key: $ME_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "otp": "884213" }' ``` ```json { "id": "0f3c…", "verified": false, "status": "pending", "attempts_remaining": 2 } ``` ::: warning A wrong code is `200`, not `4xx` The request was well formed; only the code was wrong. Branch on `verified`, not on the status code. ::: `attempts_remaining` is **absent** once the session locks — which is not the same as `0`. A UI rendering "N attempts left" should show nothing there. ## Verify a code issued elsewhere ```bash curl -X POST "$ME_BASE/otp/verify" \ -H "API-Key: $ME_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone_number": "+66886578553", "code": "884213", "ref_code": "A1B2", "purpose": "signup" }' ``` | Field | Notes | | -------------- | ---------------------------------------------------------- | | `phone_number` | E.164. | | `code` | The code the user entered. | | `ref_code` | The reference issued when the code was created. | | `email` | Optional. | | `purpose` | Optional claim namespace, e.g. `signup`, `welcome_credit`. | ```json { "is_valid": true, "status": "verified", "message": "OK", "is_new": true } ``` ## `is_new` may be absent — that is not `false` `is_new` answers "was this phone number claimed for this `purpose` for the first time?" It is **omitted** when there was nothing to answer: no `purpose` was sent, or the claim store could not be reached. That distinction is load-bearing if you gate a reward on it: ```ts // Correct: absent ⇒ withhold, don't pay out on an unknown. if (res.is_new === true) grantWelcomeCredit(); ``` Treating absent as `false` is safe. Treating it as "not new" _and_ then paying out on the inverse (`if (!res.is_new)`) pays on every error instead of none. ## The API key identifies the project The key resolves to exactly one project, and the claim is recorded against it. A key that does not resolve to a project is rejected with `401` — see [Authentication](/en/overview/authentication). --- # Voice Source: /en/capabilities/voice # Voice The voice surface is split into three pages, by what you are acting on: | You want to… | Go to | | --------------------------------------------------- | -------------------------------------------- | | **Place** a call — dial, message, agent, or survey | [Place Calls](/en/capabilities/place-calls) | | Control a call that is **already in progress** | [Live Call Control](/en/capabilities/calls) | | Configure **agents**, or read the voice catalog | [Voice Agents](/en/capabilities/agents) | ## Placing calls Each kind of call has its own endpoint under `/telephony/call/…` — ring a number, speak a message, connect an AI agent, or run a keypad or spoken survey. Send an `Idempotency-Key` so a retry never dials twice. → [Place Calls](/en/capabilities/place-calls) ## Call control Calls in progress are addressed by **LiveKit room name**, not by session id — the room is what exists while people are still talking. Hold, supervisor mode, hangup, lock, and participant removal all live there. → [Live Call Control](/en/capabilities/calls) ## Agents and the voice catalog Agent configurations decide how an agent behaves; the catalog lists which speech providers and voices this deployment can actually use. Availability follows the vendor keys configured for your environment, so read it at runtime rather than hard-coding a provider list. → [Voice Agents](/en/capabilities/agents) ::: warning Older voice endpoints Earlier versions of this documentation described `/voice/otp`, `/voice/broadcast`, `/voice/dial`, `/voice/sessions`, and `/voice/ivr_config`. Those routes are **not part of the current API** and will return `404`. If you built against them, the pages above are the current surface. ::: --- # Webhook Deliveries Source: /en/capabilities/webhook-deliveries # Webhook Deliveries This is the **outbound** side of webhooks: what MessageEagle tried to send you, and what your endpoint answered. For receiving and acknowledging delivery reports, see [Webhooks & Delivery Reports](/en/best-practices/webhooks). | Action | Route | | ---------------- | ----------------------------------------------------------------------- | | Delivery history | `GET /organization/{organizationId}/webhook-deliveries` | | Replay one | `POST /organization/{organizationId}/webhook-deliveries/{deliveryId}/replay` | | Test an endpoint | `POST /webhook-test` | `{organizationId}` is the same value you send as `X-TenantID`. ## Delivery history ```bash curl "$ME_BASE/organization/$ME_TENANT/webhook-deliveries?status=failed&limit=50" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` | Query | Filters by | | ------------- | -------------------------------- | | `endpoint_id` | One configured endpoint. | | `event_id` | One event. | | `status` | Delivery outcome. | | `q` | Free text. | | `limit`, `offset` | Paging. | ## Replay Re-send a delivery your endpoint missed — useful after you fix a bug that made it return 5xx. ```bash curl -X POST "$ME_BASE/organization/$ME_TENANT/webhook-deliveries/$DELIVERY_ID/replay" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` ## Test an endpoint Fire a one-off request at a URL and see exactly what it answers, before you point real events at it. ```bash curl -X POST "$ME_BASE/webhook-test" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/me", "method": "POST", "headers": [{ "key": "X-Secret", "value": "s3cr3t" }], "body": { "ping": true } }' ``` ```json { "success": true, "status": 200, "statusText": "OK", "headers": { "content-type": "application/json" }, "data": { "ok": true } } ``` ::: warning `success` is not `2xx` `success` reports that the request **completed**, not that your endpoint accepted it. Check `status` as well. ::: Allowed methods: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`. ## Go SDK ```go history, err := c.Webhooks.ListDeliveries(ctx, &sdk.ListDeliveriesOptions{Status: "failed"}) _, err = c.Webhooks.Replay(ctx, deliveryID) res, err := c.Webhooks.Test(ctx, &sdk.WebhookTestRequest{URL: url, Method: "POST"}) ``` --- # World Data Source: /en/capabilities/world-data # World Data **World Data** is reference data: facts that are the same for every caller and change a few times a year. Today that is the country dial-code registry. It exists so a phone-number input does not need its own copy of the list. A hard-coded table goes stale silently — nobody notices a missing country until someone from it cannot sign up. | Action | Route | | ------------------- | --------------------------- | | List country codes | `GET /worlddata/countries` | ::: info Also accepts a publishable key A dial-code registry is the same for every caller and grants nothing, so this is the one surface that also admits a browser-safe publishable key (`pk_…`) from an Origin that key is locked to. It uses the `API-Key` header instead. ::: ## List country dial codes ``` GET /worlddata/countries?page=1&limit=100 API-Key: $ME_API_KEY ``` ```json { "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 } ``` | Field | Meaning | | ----------- | ----------------------------------------------------- | | `code` | ISO-3166 alpha-2, uppercase | | `dial_code` | E.164 calling code, leading `+` included | | `name` | English display name | ## Paging `page` is 1-based and both values are echoed back, so a client can walk `total_pages` without tracking its own cursor. | Input | Behaviour | | ---------------------- | ------------------------------------------ | | `limit` above 250 | Capped at 250 | | `limit` or `page` junk | Falls back to the default (page 1, limit 100) | | `page` past the end | Empty `data`, still `200` | A page past the end returns an empty list rather than `404` so a client walking to the end needs no special case for the row after the last one. ::: warning NANP territories keep their full prefix Countries in the North American Numbering Plan share country code `1` but have distinct prefixes — American Samoa is `+1684`, Anguilla `+1264`. Do not truncate these to `+1`; the resulting numbers are unroutable. ::: ## Caching This registry changes a few times a year. Cache it for as long as suits you and refresh on deploy — there is no need to fetch it per request. --- # Authentication Source: /en/overview/authentication # Authentication Every request to the Platform API carries two required headers. | Header | Required | Purpose | | ------------ | -------- | ---------------------------------------------------- | | `API-Key` | ✅ | Your platform API key, from the developer console. | | `X-TenantID` | ✅ | The organization (workspace) the request runs in. | ```bash curl "$ME_BASE/voice/providers" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` ::: danger Keep credentials server-side `API-Key` is a secret. Never embed it in a browser, mobile app, or any client you don't control. Proxy requests through your own backend. ::: ## What a key can reach A key carries its **owner's** permissions. The request is authorized as the user who owns the key, against the organization in `X-TenantID` — so a key reaches exactly what its owner reaches, and no more. It is not a narrower scope than that user. Issue keys from an account whose access matches what the integration actually needs, and rotate on any leak. ## Errors | Status | Meaning | Fix | | ------ | ------------------------------------------- | ----------------------------------------------------- | | `401` | Missing or invalid `API-Key` / `X-TenantID` | Check both headers are present and current. | | `400` | Malformed body | Validate against the [API Reference](/en/api-reference). | | `404` | Unknown resource | Verify the path and any `{uuid}`. | Error responses share one shape: ```json { "code": "invalid_request", "message": "phone_number is required", "type": "validation" } ``` ## Base URLs | Environment | Base URL | | ----------- | ------------------------------------------------------- | | Production | `https://api.messageeagle.com/api/v1/app1/platform` | | Sandbox | `https://sandbox.messageeagle.com/api/v1/app1/platform` | Continue to [SMS Messaging](/en/capabilities/sms). --- # Introduction Source: /en/overview/introduction # Introduction MessageEagle is a messaging platform for **SMS**, **Voice**, and **Verify (OTP)**. One set of credentials and one base URL give you transactional and broadcast messaging, voice calls and IVR, and one-time-password delivery and verification across every major carrier. ::: tip In a hurry? Jump straight to the [Quickstart](/en/overview/quickstart) to send your first message, or open the interactive [API Reference](/en/api-reference) to try endpoints in your browser. ::: ## How the API is shaped Every business endpoint lives under one base path: ``` https://api.messageeagle.com/api/v1/app1/platform ``` Requests and responses are JSON. Two headers authenticate every call — see [Authentication](/en/overview/authentication). | Surface | What it does | Start here | | ---------------- | ----------------------------------------------- | ------------------------------------------ | | **SMS** | Transactional, broadcast, and EagleEye messages | [SMS Messaging](/en/capabilities/sms) | | **Voice** | OTP calls, broadcasts, agent dialing, IVR | [Voice](/en/capabilities/voice) | | **Verify** | Send and check one-time passwords | [Verify / OTP](/en/capabilities/verify) | | **Sender Names** | Register and approve sender IDs | [Sender Names](/en/capabilities/sender-names) | ## Design principles - **Per-item results.** Bulk send endpoints return a status for **each** recipient. One bad number never fails the whole batch — inspect `send_messages[].report` per item. - **Idempotency via `transaction_uuid`.** Attach a UUIDv7 to each message to correlate delivery-report webhooks and later tracking calls. - **Templating built in.** Message bodies support {{first_name}}-style placeholders resolved from per-recipient fields and `meta_data`. ## What you'll need 1. An **API key** (`API-Key`) and **organization id** (`X-TenantID`). 2. At least one **approved sender name** — see [Sender Names](/en/capabilities/sender-names). 3. A recipient in **E.164** format, e.g. `+66886578553`. Ready? Continue to the [Quickstart](/en/overview/quickstart). --- # Quickstart Source: /en/overview/quickstart # Quickstart This guide sends a single transactional SMS. You'll need your `API-Key`, `X-TenantID`, and one [approved sender name](/en/capabilities/sender-names). ## 1. Set your credentials ```bash export ME_API_KEY="your-app-token" export ME_TENANT="your-tenant-id" export ME_BASE="https://api.messageeagle.com/api/v1/app1/platform" ``` ## 2. Send a message ::: code-group ```bash [cURL] curl -X POST "$ME_BASE/sms/send_messages/message" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "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" } ] }' ``` ```go [Go SDK] package main import ( "context" "log" "github.com/lodashventure/messageeagle/sms-voice/sdk" ) func main() { client, err := sdk.New(sdk.Config{ BaseURL: "https://api.messageeagle.com", APIKey: "your-api-key", TenantID: "your-organization-id", }) if err != nil { log.Fatal(err) } resp, err := client.SMS.SendMessage(context.Background(), &sdk.SendMessageRequest{ CheckSpamMsgMode: "not_check", Data: []sdk.SendMessageItem{{ SenderName: "MyBrand", Operator: "ais", PhoneNumber: "+66886578553", Message: "Hi Mali, your order has shipped.", }}, }) if err != nil { log.Fatal(err) } log.Printf("accepted=%d rejected=%d batches=%+v", resp.Accepted, resp.Rejected, resp.Batches) } ``` ```ts [Fetch] const res = await fetch(`${ME_BASE}/sms/send_messages/message`, { method: "POST", headers: { "API-Key": ME_API_KEY, "X-TenantID": ME_TENANT, "Content-Type": "application/json", }, body: JSON.stringify({ 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", }, ], }), }); console.log(await res.json()); ``` ::: ## 3. Read the result The response reports the fan-out, wrapped in a `data` envelope. Rows sharing operator + sender + message become one batch each: ```json { "data": { "accepted": 1, "rejected": 0, "batches": [ { "batch_id": "018f7b2a-7c3e-7a1b-9c2d-1a2b3c4d5e6f", "accepted": 1, "rejected": 0 } ] } } ``` ::: tip `undeliverable_chars` A batch may also carry `undeliverable_chars` — codepoints the carrier is known to destroy. The loss is otherwise invisible: the message is accepted, the unit is billed, and the handset shows nothing there. ::: ::: warning Sender name must be approved If `sender_name` is not approved for the target carrier, those recipients are counted in `rejected` rather than failing the whole request. ::: ## Next steps - [Authentication](/en/overview/authentication) — header details and errors. - [SMS Messaging](/en/capabilities/sms) — broadcasts, scheduling, templating. - [Webhooks & Delivery Reports](/en/best-practices/webhooks) — get notified when messages land. --- # Table of Contents Source: /en/table-of-content # Table of Contents Everything in the MessageEagle documentation, on a single page. Use it as a map, or hand the whole site to an LLM via the **LLMs full** option in the Copy page menu above. ## Get Started - [Introduction](/en/overview/introduction) — what MessageEagle is and how the API is shaped. - [Quickstart](/en/overview/quickstart) — send your first message in under five minutes. - [Authentication](/en/overview/authentication) — the `API-Key` and `X-TenantID` headers. ## Capabilities - [SMS Messaging](/en/capabilities/sms) — transactional, broadcast, and EagleEye sends. - [Voice](/en/capabilities/voice) — OTP, broadcasts, agent dialing, sessions, and IVR. - [Place Calls](/en/capabilities/place-calls) — dial, speak a message, connect an agent, or run a survey. - [Live Call Control](/en/capabilities/calls) — hold, supervisor mode, hangup, and participants on a call in progress. - [Voice Agents](/en/capabilities/agents) — agent configurations and the provider/voice catalog. - [Verify / OTP](/en/capabilities/verify) — send and verify one-time passwords. - [Sender Names](/en/capabilities/sender-names) — register and approve sender IDs. - [World Data](/en/capabilities/world-data) — country dial codes and other reference data. - [Webhook Deliveries](/en/capabilities/webhook-deliveries) — delivery history, replay, and endpoint testing. ## Best Practices - [Messaging Best Practices](/en/best-practices/best-practices) — write, route, and template messages that land. - [Rate Limits & Quotas](/en/best-practices/rate-limits) — how throttling works and how to stay under it. - [Webhooks & Delivery Reports](/en/best-practices/webhooks) — receive status callbacks reliably. ## Reference - [API Reference](/en/api-reference) — interactive, “try it” reference for every endpoint. - [OpenAPI document](/openapi.yaml) — the raw OpenAPI 3.1 spec. - [llms.txt](/llms.txt) · [llms-full.txt](/llms-full.txt) — machine-readable indexes for LLMs. --- # ja/index Source: /ja --- # 音声エージェント Source: /ja/capabilities/agents # 音声エージェント ここには 2 つのものがあります。エージェントの振る舞いを決める**設定**と、エージェントが話すために使える**音声とプロバイダーの一覧**です。 | 操作 | Route | | ---------------- | ---------------------------------------- | | 設定の一覧 | `GET /agents/configs` | | 設定の取得 | `GET /agents/configs/{documentId}` | | 作成 | `POST /agents/configs` | | 置き換え | `PUT /agents/configs/{documentId}` | | 削除 | `DELETE /agents/configs/{documentId}` | | プロバイダー一覧 | `GET /voice/providers` | | 音声一覧 | `GET /voice/providers/{provider}/voices` | ## 設定 設定はひとつのドキュメントです。エージェントの機能が増えると studio がフィールドを追加するため、知らないキーは削らずそのまま渡してください。 ## プロバイダーと音声の一覧 利用できるプロバイダーは**デプロイごとに異なります**。固定値ではなく、その環境に設定された vendor key によって決まります。プロバイダー名をコードに書き込まず、実行時に一覧を取得してください。 ```json { "data": [ { "id": "voice_abc", "name": "Mali", "gender": "female", "language": "th-TH", "preview": true } ] } ``` ::: tip `preview` に従ってください `preview` は vendor key によって変わるため、リクエストごとに判定されます。`false` のときは再生ボタンを無効にしてください。別の音声で代替すると、エージェントが実際には使わない声を聞かせることになります。 ::: --- # 通話の制御 Source: /ja/capabilities/calls # 通話の制御 このエンドポイント群は**すでに通話中**の呼に対して動作します。呼び出しには session id ではなく LiveKit のルーム名を使います。通話が続いている間、実体として存在するのはルームだからです。 | 操作 | Route | 用途 | | ---------------- | -------------------------------------------------------- | ---------------------------------------- | | **保留** | `POST /voice/calls/{roomName}/hold` | 相手を保留にする、または再開する | | **モード** | `POST /voice/calls/{roomName}/mode` | スーパーバイザーの聴取モードを切り替える | | **切断** | `POST /voice/calls/{roomName}/hangup` | 通話を終了し session を確定する | | **ロック** | `POST /voice/calls/{roomName}/lock` | 以降の参加を止める | | **参加者の削除** | `DELETE /voice/calls/{roomName}/participants/{identity}` | 参加者を 1 名退出させる | | **強制クローズ** | `POST /voice/calls/{uuid}/force-close` | ルームが既に無い session を閉じる | ::: danger API キーは通話中の呼を操作できます この経路では、キーは**所有者の権限**を持ちます。キーを入手した者は通話中の呼を切断したり、引き継いだりできます。サーバー側にのみ保管し、漏洩したら直ちに再発行してください。 ::: ## 保留と再開 保留は 2 つのことを行います。両者の音声をミュートし、保留音を流します。ミュートは保留音が設定されていなくても動作するため、保留中の相手にオペレーター側の音声が聞こえることはありません。 ```bash curl -X POST "$ME_BASE/voice/calls/$ROOM/hold" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{"muted": true, "holder_identity": "agent-42"}' ``` ```json { "status": "ok", "held": true } ``` 同じ route に `"muted": false` を送ると再開します。 --- # Webhook 配信 Source: /ja/capabilities/webhook-deliveries # Webhook 配信 このページは webhook の**送信側**です。MessageEagle が何を送ろうとし、相手のエンドポイントがどう応答したかを扱います。配信レポートの受信については英語版の [Webhooks & Delivery Reports](/en/best-practices/webhooks) を参照してください。 | 操作 | Route | | ---------------------- | ---------------------------------------------------------------------------- | | 配信履歴 | `GET /organization/{organizationId}/webhook-deliveries` | | 再送 | `POST /organization/{organizationId}/webhook-deliveries/{deliveryId}/replay` | | エンドポイントのテスト | `POST /webhook-test` | `{organizationId}` は `X-TenantID` に送るものと同じ値です。 ## エンドポイントをテストする 実際のイベントを流す前に、URL へ 1 回だけリクエストを送って応答を確認します。 ```bash curl -X POST "$ME_BASE/webhook-test" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/me", "method": "POST", "headers": [{ "key": "X-Secret", "value": "s3cr3t" }], "body": { "ping": true } }' ``` ::: warning `success` は `2xx` を意味しません `success` はリクエストが**完了した**ことを示すだけで、相手が受理したことは示しません。`status` も確認してください。 ::: --- # 認証 Source: /ja/overview/authentication # 認証 Platform API へのすべてのリクエストに、次の 2 つのヘッダーが必要です。 | ヘッダー | 必須 | 用途 | | ------------ | ---- | ------------------------------------------------- | | `API-Key` | ✅ | 開発者コンソールで発行した API キー | | `X-TenantID` | ✅ | リクエストが実行される組織 (ワークスペース) の ID | ```bash curl "$ME_BASE/voice/providers" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` ::: danger 認証情報はサーバー側に保管してください `API-Key` は秘密情報です。ブラウザやモバイルアプリなど、管理下にないクライアントに埋め込まないでください。自社のバックエンド経由でリクエストしてください。 ::: ## キーで到達できる範囲 キーは**その所有者の権限**を持ちます。リクエストはキーの所有者として、`X-TenantID` の組織に対して認可されます。つまりキーが到達できる範囲は所有者と同じで、それ以上ではありません。 キー自体が権限を絞るわけではありません。連携に必要な権限だけを持つアカウントでキーを発行し、漏洩した場合はすぐに再発行してください。 ## エラー | ステータス | 意味 | 対処 | | ---------- | ------------------------------------------ | ----------------------------------------------- | | `401` | `API-Key` または `X-TenantID` が無いか不正 | 両方のヘッダーが存在し有効か確認します | | `400` | リクエストボディが不正 | [API リファレンス](/en/api-reference) と照合します | | `404` | 対象が見つからない | パスと ID を確認します | --- # はじめに Source: /ja/overview/introduction # はじめに MessageEagle は **SMS**・**音声**・**本人確認 (OTP)** のためのメッセージング プラットフォームです。ひとつの認証情報とひとつのベース URL で、メッセージ送信、通話の制御、音声エージェントの設定を扱えます。 ::: tip すぐ試したい場合 [クイックスタート](/ja/overview/quickstart) から最初のメッセージを送るか、[API リファレンス](/en/api-reference) をブラウザで開いてください。 ::: ## API の構成 業務用エンドポイントはすべて次のベースパスの下にあります。リクエストとレスポンスは JSON で、認証は 2 つのヘッダーで行います。 ``` https://api.messageeagle.com/api/v1/app1/platform ``` | 領域 | できること | 参照 | | -------------------- | ------------------------------ | --------------------------------------------------- | | **SMS** | トランザクション送信と一斉送信 | [クイックスタート](/ja/overview/quickstart) | | **通話の制御** | 通話中の保留・モード・切断 | [通話の制御](/ja/capabilities/calls) | | **音声エージェント** | エージェント設定と音声の一覧 | [音声エージェント](/ja/capabilities/agents) | | **Webhook** | 配信履歴・再送・テスト | [Webhook 配信](/ja/capabilities/webhook-deliveries) | ## 必要なもの 1. **API キー** (`API-Key`) と **組織 ID** (`X-TenantID`) 2. 承認済みの**送信者名**がひとつ以上 3. **E.164** 形式の宛先(例: `+66886578553`) ::: info i18n この言語では一部のページのみ翻訳されています。未翻訳のページは英語版をご覧ください。 ::: --- # クイックスタート Source: /ja/overview/quickstart # クイックスタート このガイドでは SMS を 1 通送信します。`API-Key`、`X-TenantID`、そして承認済みの送信者名が必要です。 ## 1. 認証情報を設定する ```bash export ME_API_KEY="your-api-key" export ME_TENANT="your-organization-id" export ME_BASE="https://api.messageeagle.com/api/v1/app1/platform" ``` ## 2. メッセージを送信する ```bash curl -X POST "$ME_BASE/sms/send_messages/message" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "check_spam_msg_mode": "not_check", "data": [ { "sender_name": "MyBrand", "operator": "ais", "message": "Hello", "phone_number": "+66886578553" } ] }' ``` ## 3. 結果を読む レスポンスは `data` に包まれて返ります。operator・送信者名・本文が同じ行はひとつの batch にまとまります。 ```json { "data": { "accepted": 1, "rejected": 0, "batches": [ { "batch_id": "018f7b2a-7c3e-7a1b-9c2d-1a2b3c4d5e6f", "accepted": 1, "rejected": 0 } ] } } ``` ::: warning 送信者名は承認が必要です 対象キャリアで `sender_name` が未承認の場合、その宛先はリクエスト全体を失敗させず `rejected` に数えられます。 ::: ## 次のステップ - [認証](/ja/overview/authentication) - [通話の制御](/ja/capabilities/calls) - [Webhook 配信](/ja/capabilities/webhook-deliveries) --- # 目次 Source: /ja/table-of-content # 目次 ## はじめに - [はじめに](/ja/overview/introduction) — MessageEagle の概要と API の構成 - [クイックスタート](/ja/overview/quickstart) — 最初のメッセージを 5 分以内に送信する - [認証](/ja/overview/authentication) — `API-Key` と `X-TenantID` ヘッダー ## 機能 - [通話の制御](/ja/capabilities/calls) — 通話中の保留・モード・切断・参加者の操作 - [音声エージェント](/ja/capabilities/agents) — エージェント設定と音声の一覧 - [Webhook 配信](/ja/capabilities/webhook-deliveries) — 配信履歴・再送・エンドポイントのテスト ## リファレンス - [API リファレンス](/en/api-reference) — ブラウザで試せる対話型リファレンス(英語) ::: info i18n この言語では一部のページのみ翻訳されています。未翻訳のページは英語版をご覧ください。 ::: --- # ko/index Source: /ko --- # 음성 에이전트 Source: /ko/capabilities/agents # 음성 에이전트 여기에는 두 가지가 있습니다. 에이전트의 동작을 정하는 **설정**과, 에이전트가 말할 때 쓰는 **음성·공급자 목록**입니다. | 동작 | Route | | ----------- | ---------------------------------------- | | 설정 목록 | `GET /agents/configs` | | 설정 조회 | `GET /agents/configs/{documentId}` | | 생성 | `POST /agents/configs` | | 교체 | `PUT /agents/configs/{documentId}` | | 삭제 | `DELETE /agents/configs/{documentId}` | | 공급자 목록 | `GET /voice/providers` | | 음성 목록 | `GET /voice/providers/{provider}/voices` | ## 설정 설정은 하나의 문서입니다. 에이전트 기능이 늘어나면 studio가 필드를 추가하므로, 모르는 키는 버리지 말고 그대로 전달하세요. ## 공급자와 음성 목록 사용 가능한 공급자는 **배포 환경마다 다릅니다**. 고정값이 아니라 그 환경에 설정된 vendor key에 따라 정해집니다. 공급자 이름을 코드에 넣지 말고 실행 시점에 목록을 읽으세요. ```json { "data": [ { "id": "voice_abc", "name": "Mali", "gender": "female", "language": "th-TH", "preview": true } ] } ``` ::: tip `preview` 값을 따르세요 `preview`는 vendor key에 따라 달라지므로 요청마다 계산됩니다. `false`면 재생 버튼을 비활성화하세요. 다른 음성으로 대체하면 에이전트가 실제로 쓰지 않는 목소리를 들려주게 됩니다. ::: --- # 통화 제어 Source: /ko/capabilities/calls # 통화 제어 이 엔드포인트들은 **이미 진행 중인** 통화에 작용합니다. session id가 아니라 LiveKit 룸 이름으로 지정합니다. 통화가 이어지는 동안 실제로 존재하는 것은 룸이기 때문입니다. | 동작 | Route | 용도 | | --------------- | -------------------------------------------------------- | ----------------------------------- | | **보류** | `POST /voice/calls/{roomName}/hold` | 상대를 보류하거나 다시 연결합니다 | | **모드** | `POST /voice/calls/{roomName}/mode` | 수퍼바이저 청취 모드를 바꿉니다 | | **종료** | `POST /voice/calls/{roomName}/hangup` | 통화를 끝내고 session을 마감합니다 | | **잠금** | `POST /voice/calls/{roomName}/lock` | 추가 참가를 막습니다 | | **참가자 제거** | `DELETE /voice/calls/{roomName}/participants/{identity}` | 참가자 한 명을 내보냅니다 | | **강제 종료** | `POST /voice/calls/{uuid}/force-close` | 룸이 이미 사라진 session을 닫습니다 | ::: danger API 키로 진행 중인 통화를 제어할 수 있습니다 이 경로에서 키는 **소유자의 권한**을 가집니다. 키를 가진 사람은 진행 중인 통화를 끊거나 넘겨받을 수 있습니다. 서버에만 보관하고, 유출되면 즉시 재발급하세요. ::: ## 보류와 재개 보류는 두 가지를 합니다. 양쪽 음성을 음소거하고 보류음을 재생합니다. 음소거는 보류음이 설정되지 않아도 동작하므로, 보류된 상대에게 상담원 쪽 소리가 들리지 않습니다. ```bash curl -X POST "$ME_BASE/voice/calls/$ROOM/hold" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{"muted": true, "holder_identity": "agent-42"}' ``` ```json { "status": "ok", "held": true } ``` 같은 route에 `"muted": false`를 보내면 다시 연결됩니다. --- # Webhook 전송 Source: /ko/capabilities/webhook-deliveries # Webhook 전송 이 페이지는 webhook의 **보내는 쪽**입니다. MessageEagle이 무엇을 보내려 했고 엔드포인트가 어떻게 응답했는지를 다룹니다. 전송 리포트 수신은 영어 문서 [Webhooks & Delivery Reports](/en/best-practices/webhooks)를 참고하세요. | 동작 | Route | | ----------------- | ---------------------------------------------------------------------------- | | 전송 내역 | `GET /organization/{organizationId}/webhook-deliveries` | | 재전송 | `POST /organization/{organizationId}/webhook-deliveries/{deliveryId}/replay` | | 엔드포인트 테스트 | `POST /webhook-test` | `{organizationId}`는 `X-TenantID`로 보내는 값과 같습니다. ## 엔드포인트 테스트 실제 이벤트를 연결하기 전에 URL로 한 번만 요청을 보내 응답을 확인합니다. ```bash curl -X POST "$ME_BASE/webhook-test" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/me", "method": "POST", "headers": [{ "key": "X-Secret", "value": "s3cr3t" }], "body": { "ping": true } }' ``` ::: warning `success`는 `2xx`를 뜻하지 않습니다 `success`는 요청이 **완료되었다**는 뜻일 뿐, 엔드포인트가 받아들였다는 뜻은 아닙니다. `status`도 함께 확인하세요. ::: --- # 인증 Source: /ko/overview/authentication # 인증 Platform API로 보내는 모든 요청에는 두 개의 헤더가 필요합니다. | 헤더 | 필수 | 용도 | | ------------ | ---- | ------------------------------------- | | `API-Key` | ✅ | 개발자 콘솔에서 발급한 API 키 | | `X-TenantID` | ✅ | 요청이 실행되는 조직(워크스페이스) ID | ```bash curl "$ME_BASE/voice/providers" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` ::: danger 인증 정보는 서버에만 두세요 `API-Key`는 비밀 정보입니다. 브라우저나 모바일 앱처럼 통제할 수 없는 클라이언트에 넣지 마세요. 자체 백엔드를 통해 요청하세요. ::: ## 키가 닿는 범위 키는 **소유자의 권한**을 그대로 가집니다. 요청은 키 소유자로서 `X-TenantID`의 조직에 대해 인가됩니다. 즉 키가 닿는 범위는 소유자와 같고, 그 이상은 아닙니다. 키 자체가 권한을 좁혀 주지는 않습니다. 연동에 필요한 권한만 가진 계정으로 키를 발급하고, 유출되면 즉시 재발급하세요. ## 오류 | 상태 | 의미 | 해결 | | ----- | -------------------------------------------------- | ---------------------------------------------- | | `401` | `API-Key` 또는 `X-TenantID`가 없거나 올바르지 않음 | 두 헤더가 모두 있고 유효한지 확인합니다 | | `400` | 요청 본문이 잘못됨 | [API 레퍼런스](/en/api-reference)와 대조합니다 | | `404` | 대상을 찾을 수 없음 | 경로와 ID를 확인합니다 | --- # 소개 Source: /ko/overview/introduction # 소개 MessageEagle은 **SMS**, **음성**, **본인확인(OTP)** 을 위한 메시징 플랫폼입니다. 하나의 인증 정보와 하나의 베이스 URL로 메시지 발송, 통화 제어, 음성 에이전트 설정을 다룹니다. ::: tip 바로 써보려면 [빠른 시작](/ko/overview/quickstart)에서 첫 메시지를 보내거나, [API 레퍼런스](/en/api-reference)를 브라우저에서 열어 보세요. ::: ## API 구성 모든 업무용 엔드포인트는 아래 베이스 경로 아래에 있습니다. 요청과 응답은 JSON이며, 인증은 두 개의 헤더로 합니다. ``` https://api.messageeagle.com/api/v1/app1/platform ``` | 영역 | 하는 일 | 시작점 | | ----------------- | ------------------------- | --------------------------------------------------- | | **SMS** | 트랜잭션 발송과 대량 발송 | [빠른 시작](/ko/overview/quickstart) | | **통화 제어** | 통화 중 보류·모드·종료 | [통화 제어](/ko/capabilities/calls) | | **음성 에이전트** | 에이전트 설정과 음성 목록 | [음성 에이전트](/ko/capabilities/agents) | | **Webhook** | 전송 내역·재전송·테스트 | [Webhook 전송](/ko/capabilities/webhook-deliveries) | ## 준비물 1. **API 키**(`API-Key`)와 **조직 ID**(`X-TenantID`) 2. 승인된 **발신자 이름** 하나 이상 3. **E.164** 형식의 수신 번호(예: `+66886578553`) ::: info i18n 이 언어에서는 일부 페이지만 번역되어 있습니다. 번역되지 않은 페이지는 영어 문서를 참고하세요. ::: --- # 빠른 시작 Source: /ko/overview/quickstart # 빠른 시작 이 문서는 SMS 한 통을 보냅니다. `API-Key`, `X-TenantID`, 그리고 승인된 발신자 이름이 필요합니다. ## 1. 인증 정보 설정 ```bash export ME_API_KEY="your-api-key" export ME_TENANT="your-organization-id" export ME_BASE="https://api.messageeagle.com/api/v1/app1/platform" ``` ## 2. 메시지 보내기 ```bash curl -X POST "$ME_BASE/sms/send_messages/message" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "check_spam_msg_mode": "not_check", "data": [ { "sender_name": "MyBrand", "operator": "ais", "message": "Hello", "phone_number": "+66886578553" } ] }' ``` ## 3. 결과 읽기 응답은 `data`로 감싸여 돌아옵니다. operator·발신자 이름·본문이 같은 행은 하나의 batch로 묶입니다. ```json { "data": { "accepted": 1, "rejected": 0, "batches": [ { "batch_id": "018f7b2a-7c3e-7a1b-9c2d-1a2b3c4d5e6f", "accepted": 1, "rejected": 0 } ] } } ``` ::: warning 발신자 이름은 승인이 필요합니다 대상 통신사에서 `sender_name`이 승인되지 않았다면 해당 수신자는 요청 전체를 실패시키지 않고 `rejected`에 집계됩니다. ::: ## 다음 단계 - [인증](/ko/overview/authentication) - [통화 제어](/ko/capabilities/calls) - [Webhook 전송](/ko/capabilities/webhook-deliveries) --- # 목차 Source: /ko/table-of-content # 목차 ## 시작하기 - [소개](/ko/overview/introduction) — MessageEagle 개요와 API 구성 - [빠른 시작](/ko/overview/quickstart) — 5분 안에 첫 메시지 보내기 - [인증](/ko/overview/authentication) — `API-Key`와 `X-TenantID` 헤더 ## 기능 - [통화 제어](/ko/capabilities/calls) — 통화 중 보류·모드·종료·참가자 관리 - [음성 에이전트](/ko/capabilities/agents) — 에이전트 설정과 음성 목록 - [Webhook 전송](/ko/capabilities/webhook-deliveries) — 전송 내역·재전송·엔드포인트 테스트 ## 레퍼런스 - [API 레퍼런스](/en/api-reference) — 브라우저에서 바로 시험하는 대화형 레퍼런스(영어) ::: info i18n 이 언어에서는 일부 페이지만 번역되어 있습니다. 번역되지 않은 페이지는 영어 문서를 참고하세요. ::: --- # th/index Source: /th --- # API Reference Source: /th/api-reference --- # แนวปฏิบัติที่ดีสำหรับการส่งข้อความ Source: /th/best-practices/best-practices # แนวปฏิบัติที่ดีสำหรับการส่งข้อความ การส่งข้อความให้ถึงผู้รับไม่ได้มีแค่การเรียก endpoint เพียงอย่างเดียว หน้านี้รวบรวมแนวปฏิบัติที่จะช่วยยกระดับจาก "ส่งแล้ว" ไปสู่ "ส่งสำเร็จสม่ำเสมอ ถูก carrier ถูกเนื้อหา" ::: tip ส่งหน้านี้ให้ AI ใช้ **Copy page** (มุมขวาบน) เพื่อคัดลอกคู่มือนี้เป็น Markdown หรือ **Open in Claude / ChatGPT / Gemini** เพื่อถามคำถามโดยตรง ::: ## เลือกประเภทการส่งให้ถูกต้อง ข้อความแต่ละประเภทไม่ควรใช้เส้นทางเดียวกัน - **Transactional** (`/sms/send_messages/message`) — ใบเสร็จ รหัสยืนยัน การแจ้งเตือน ไวต่อเวลาแฝง ควรรักษา queue นี้ให้สะอาดอยู่เสมอ - **Broadcast** (`/sms/broadcast`) — โปรโมชันและประกาศต่าง ๆ ต้องเปิด `ME_SMS_PRODUCER_ENABLED` ที่ฝั่ง deployment มี rate limit แยกต่างหาก จึงไม่กระทบการส่ง transactional - **OTP** — ส่งรหัสด้วย `/sms/send_messages/message` แล้วตรวจสอบที่ `/otp/verify` (ไม่มี endpoint สำหรับส่ง OTP โดยเฉพาะ) ::: warning อย่าส่งโปรโมชันผ่านเส้นทาง transactional การส่งโปรโมชันจำนวนมากผ่าน `/message` จะแย่งคิวกับ OTP และใบเสร็จที่ต้องการความเร็ว ควรใช้ `/broadcast` สำหรับการตลาดเสมอ ::: ## ใช้เทมเพลตแทนการเชื่อมสตริง สร้างเนื้อหาข้อความด้วย placeholder แทนการต่อสตริง platform จะแทนที่ {{first_name}} และ {{meta_data.*}} ตามผู้รับแต่ละคน ทำให้ payload เดียวปรับแต่งเนื้อหาให้ทั้ง batch ได้ ```json { "message": "Hi {{first_name}}, your balance is {{meta_data.balance}}.", "first_name": "Mali", "meta_data": { "balance": "1,250" } } ``` ## แนบ `transaction_uuid` ทุกครั้ง UUIDv7 ต่อข้อความหนึ่งชุดคือด้ายเส้นเดียวที่เชื่อมโยง: 1. การเรียก send 2. [webhook รายงานการส่ง](/th/best-practices/webhooks) 3. การค้นหาสถานะในภายหลัง หากไม่มี `transaction_uuid` จะไม่สามารถเชื่อมโยง status callback กลับไปยังข้อความต้นทางได้ ## กำหนดเส้นทางตาม carrier อย่างตั้งใจ `operator` ระบุ carrier (`ais`, `dtac`, `true_corp`) ตรวจสอบให้แน่ใจว่า `sender_name` [ได้รับการอนุมัติ](/th/capabilities/sender-names) สำหรับ carrier นั้น — หากคู่นี้ยังไม่ได้รับอนุมัติ รายการนั้นจะล้มเหลว ขณะที่รายการที่เหลือใน batch ยังส่งได้ตามปกติ ## ใช้การตรวจสแปมสำหรับเนื้อหาจากผู้ใช้ หากเนื้อหาข้อความมาจากผู้ใช้ของคุณ ให้กำหนด `check_spam_msg_mode`: - `single` — ยกเลิกเฉพาะรายการที่มีปัญหา - `bulk` — ปฏิเสธทั้ง batch หากมีรายการใดไม่ผ่าน ## ยืนยัน OTP อย่างรัดกุม - ตั้ง `expired_time` ให้สั้น (3–5 นาที) และมีช่องทางขอรหัสใหม่ที่ชัดเจน - เก็บ `ref_code` ไว้ เพราะต้องใช้ในการยืนยัน - อย่า log รหัส OTP แบบเต็มร่วมกับหมายเลขโทรศัพท์ ## รายการตรวจสอบก่อนส่ง - [ ] `sender_name` ได้รับการอนุมัติสำหรับ `operator` ปลายทาง - [ ] `phone_number` อยู่ในรูปแบบ E.164 (`+66…`) - [ ] กำหนด `transaction_uuid` เพื่อใช้ติดตาม - [ ] เลือกประเภทการส่งที่ถูกต้อง (`message`, `broadcast` หรือ `otp`) - [ ] เลือกโหมดตรวจสแปมสำหรับเนื้อหาจากผู้ใช้ - [ ] มี [webhook](/th/best-practices/webhooks) รับรายงานการส่ง ## ขั้นตอนถัดไป - [Rate Limits & Quotas](/th/best-practices/rate-limits) - [Webhooks & รายงานการส่ง](/th/best-practices/webhooks) - [API Reference](/th/api-reference) --- # Rate Limits & Quotas Source: /th/best-practices/rate-limits # Rate Limits & Quotas การส่งข้อความถูกจำกัดอัตราต่อ tenant และต่อ queue โดย traffic ประเภท transactional และ broadcast จะถูกนับแยกกัน ดังนั้นการส่ง broadcast จำนวนมากจึงไม่บริโภค quota ของ transactional ## ออกแบบระบบให้รองรับ throttling - **รวมเป็น batch ภายใน request เดียว** endpoint การส่งแต่ละตัวรับ array ใน `data` (ปัจจุบันสูงสุด 100 รายการ) — ควรส่ง 1 ครั้งที่มีหลายรายการแทนการส่งหลายครั้งแบบรายการเดียว - **Backoff เมื่อได้รับ `429`** ให้ถือว่า rate-limit response เป็นเหตุการณ์ชั่วคราว retry ด้วย exponential backoff และ jitter โดยจำกัดไม่เกิน ~5 ครั้ง - **ทำให้ retry เป็น idempotent** ใช้ `transaction_uuid` เดิมเมื่อ retry เพื่อป้องกันการส่งซ้ำ ```ts async function sendWithRetry(body: unknown, attempt = 0): Promise { const res = await fetch(`${ME_BASE}/sms/send_messages/message`, { method: "POST", headers: { "X-Token": ME_TOKEN, "X-TenantID": ME_TENANT, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (res.status === 429 && attempt < 5) { const wait = Math.min(2 ** attempt * 250, 8000) + Math.random() * 250; await new Promise((r) => setTimeout(r, wait)); return sendWithRetry(body, attempt + 1); } return res; } ``` ::: warning อย่า retry แบบวนซ้ำทันที การ retry ทันทีเมื่อได้รับ `429` จะยิ่งทำให้ throttling แย่ลง ควรรอและเพิ่ม jitter เสมอ เพื่อป้องกัน client หลาย ๆ ตัว retry พร้อมกัน ::: ## การจัดกำหนดการช่วยกระจาย spike สำหรับการส่งจำนวนมากที่มีเวลาเป้าหมายในอนาคต ให้กำหนด `digest` และปล่อยให้ platform กระจายการส่งแทนที่จะยิงทุกอย่างพร้อมกัน หากต้องการยกเลิกก่อนการส่งออก ให้ใช้ `GET /sms/broadcast/{batchId}` เพื่อดูสถานะ batch --- # Webhooks & รายงานการส่ง Source: /th/best-practices/webhooks # Webhooks & รายงานการส่ง การส่งสำเร็จหมายถึง "รับไว้เพื่อส่ง" ไม่ใช่ "ส่งถึงแล้ว" รายงานการส่ง (DR) จะมาถึงแบบ asynchronous ในรูปแบบ webhook โดยอ้างอิงจาก `transaction_uuid` ที่คุณกำหนดบนข้อความ ## รับรายงานการส่ง ชี้ HTTPS endpoint สาธารณะมายัง MessageEagle และจัดการ callback: ```ts app.post("/webhooks/messageeagle", (req, res) => { const { transaction_uuid, status } = req.body; // 1. ตอบรับเร็ว — ส่ง 2xx ก่อนทำงานหนัก res.sendStatus(200); // 2. ค้นหาข้อความเดิมด้วย transaction_uuid และอัปเดตสถานะ queue.enqueue({ transaction_uuid, status }); }); ``` ## ออกแบบให้รองรับกรณียาก Webhook ส่งแบบ **at-least-once** ออกแบบ handler ทุกตัวให้รองรับสิ่งนี้ - **อัปเดตแบบ idempotent** ใช้ `transaction_uuid` เป็น key — การรับ DR เดิมสองครั้ง ต้องไม่มีผลใด ๆ เพิ่มเติม ควรใช้ UPSERT แทน INSERT - **ตอบรับก่อน ประมวลผลทีหลัง** ส่ง `2xx` ทันที แล้วทำงานจริงใน queue handler ที่ช้าทำให้เกิดการส่ง webhook ซ้ำ - **รองรับการมาผิดลำดับ** `delivered` อาจมาก่อน `sent` ใช้ลำดับความสำคัญของสถานะ ไม่ใช่แบบ last-write-wins ::: warning ยืนยันแหล่งที่มา ตรวจสอบ signature ของ request (หรือ shared secret) ก่อนเชื่อถือ payload อย่าดำเนินการกับ webhook body ที่ยังยืนยันตัวตนไม่ได้ ::: ## รูปแบบสถานะ | สถานะ | ความหมาย | | ----------- | ------------------------------------------------ | | `queued` | รับไว้แล้ว รอการส่งออก | | `sent` | ส่งถึง carrier แล้ว | | `delivered` | ยืนยันว่าถึงมือถือแล้ว | | `failed` | carrier ปฏิเสธหรือส่งไม่ได้ | | `expired` | หมดเวลา TTL ก่อนส่งถึง (เช่น มือถือติดต่อไม่ได้) | ## การสิ้นสุดของ Voice Voice session ก็ emit callback เมื่อสิ้นสุดเช่นกัน — การบันทึกเสียง transcript บันทึก DTMF และค่าใช้จ่ายจะไปปรากฏที่ [`VoiceSession`](/th/capabilities/voice) ใช้ `GET /organization/{organizationId}/webhook-deliveries` เป็น fallback หาก callback หาย — ดู [การส่ง Webhook](/th/capabilities/webhook-deliveries) --- # Voice Agent Source: /th/capabilities/agents # Voice Agent หน้านี้มีสองส่วน คือ **การตั้งค่า agent** ที่กำหนดพฤติกรรมของ agent และ **รายการ** ผู้ให้บริการเสียงกับเสียงที่ agent ใช้พูดได้ | การทำงาน | Route | | ----------------------- | ---------------------------------------- | | ดูการตั้งค่าทั้งหมด | `GET /agents/configs` | | ดูการตั้งค่ารายการเดียว | `GET /agents/configs/{documentId}` | | สร้าง | `POST /agents/configs` | | แทนที่ | `PUT /agents/configs/{documentId}` | | ลบ | `DELETE /agents/configs/{documentId}` | | ดูผู้ให้บริการ | `GET /voice/providers` | | ดูเสียงของผู้ให้บริการ | `GET /voice/providers/{provider}/voices` | ## การตั้งค่า การตั้งค่าเป็นเอกสารหนึ่งชุด studio จะเพิ่มฟิลด์เข้ามาเมื่อ agent มีความสามารถใหม่ ให้ส่งผ่านคีย์ที่ไม่รู้จักต่อไป แทนที่จะตัดทิ้ง ```bash curl "$ME_BASE/agents/configs" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` ## รายการผู้ให้บริการและเสียง ผู้ให้บริการที่ใช้ได้ขึ้นกับ**แต่ละ deployment** ไม่ใช่ค่าคงที่ เพราะขึ้นกับ vendor key ที่ตั้งค่าไว้ในระบบนั้น ให้อ่านรายการตอนใช้งานจริง แทนการเขียนรายชื่อผู้ให้บริการไว้ในโค้ด ```bash curl "$ME_BASE/voice/providers/elevenlabs/voices" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` ```json { "data": [ { "id": "voice_abc", "name": "Mali", "gender": "female", "language": "th-TH", "preview": true } ] } ``` | ฟิลด์ | หมายเหตุ | | --------- | --------------------------------------- | | `id` | ใช้เป็น voice id ของ agent | | `preview` | เสียงนี้เล่นตัวอย่างบนระบบนี้ได้หรือไม่ | ::: tip ทำตามค่า `preview` `preview` ถูกคำนวณใหม่ทุกครั้งที่เรียก เพราะเปลี่ยนตาม vendor key เมื่อเป็น `false` ให้ปิดปุ่มเล่นตัวอย่าง อย่าสลับไปเล่นเสียงอื่นแทน เพราะผู้ใช้จะได้ยินเสียงที่ agent ไม่ได้ใช้พูดจริง ::: ## Go SDK ```go configs, err := c.Agents.ListConfigs(ctx) voices, err := c.Agents.ListVoices(ctx, "elevenlabs") ``` --- # ควบคุมสายที่กำลังสนทนา Source: /th/capabilities/calls # ควบคุมสายที่กำลังสนทนา Endpoint กลุ่มนี้ทำงานกับสายที่**กำลังสนทนาอยู่** และอ้างอิงด้วยชื่อห้อง (room name) ของ LiveKit ไม่ใช่ session id เพราะห้องคือสิ่งที่มีอยู่จริงขณะที่ทั้งสองฝ่ายยังคุยกัน | การทำงาน | Route | ใช้สำหรับ | | ------------- | -------------------------------------------------------- | ----------------------------- | | **พักสาย** | `POST /voice/calls/{roomName}/hold` | พักสายผู้โทร หรือกลับมาคุยต่อ | | **โหมด** | `POST /voice/calls/{roomName}/mode` | สลับโหมดการฟังของหัวหน้างาน | | **วางสาย** | `POST /voice/calls/{roomName}/hangup` | จบสายและปิด session | | **ล็อกห้อง** | `POST /voice/calls/{roomName}/lock` | ไม่ให้มีผู้เข้าร่วมเพิ่ม | | **นำออก** | `DELETE /voice/calls/{roomName}/participants/{identity}` | นำผู้เข้าร่วมออกหนึ่งราย | | **ปิดบังคับ** | `POST /voice/calls/{uuid}/force-close` | ปิด session ที่ห้องหายไปแล้ว | ::: danger API key ควบคุมสายที่กำลังสนทนาได้ Key ถือสิทธิ์ของ**เจ้าของ key** บน route กลุ่มนี้ ผู้ที่ได้ key ไปสามารถวางสาย หรือเข้าแทรกสายที่กำลังคุยอยู่ได้ เก็บไว้ฝั่งเซิร์ฟเวอร์เท่านั้น และเปลี่ยน key ทันทีหากหลุด ::: ## พักสายและกลับมาคุยต่อ การพักสายทำสองอย่าง คือปิดเสียงทั้งสองฝั่ง และเปิดเพลงรอสาย ส่วนการปิดเสียงจะทำงาน แม้ไม่ได้ตั้งค่าเพลงรอสายไว้ ผู้โทรที่ถูกพักสายจึงไม่ได้ยินเสียงในห้องของเจ้าหน้าที่ ```bash curl -X POST "$ME_BASE/voice/calls/$ROOM/hold" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{"muted": true, "holder_identity": "agent-42"}' ``` ```json { "status": "ok", "held": true } ``` ส่ง `"muted": false` ที่ route เดิมเพื่อกลับมาคุยต่อ | ฟิลด์ | ชนิด | หมายเหตุ | | ---------------------- | ------ | ------------------------------------------------ | | `muted` | bool | `true` พักสาย `false` กลับมาคุยต่อ | | `participant_identity` | string | ไม่บังคับ จำกัดการพักสายเฉพาะผู้เข้าร่วมรายเดียว | | `track_sid` | string | ไม่บังคับ จำกัดเฉพาะ track เดียว | | `holder_identity` | string | ไม่บังคับ บันทึกลงในประวัติการโทร | ## โหมดหัวหน้างาน ```bash curl -X POST "$ME_BASE/voice/calls/$ROOM/mode" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{"mode": "whisper", "identity": "supervisor-7"}' ``` ```json { "mode": "whisper" } ``` ส่ง `exclude_identities` เพื่อยกเว้นผู้เข้าร่วมที่ไม่ต้องการให้โหมดมีผล ## วางสาย กับ ปิดบังคับ `hangup` ใช้จบสายที่กำลังคุยอยู่ โดยลบห้องและปิด session ใช้ตอนที่ยังเชื่อมต่อกันอยู่ `force-close` ใช้ปิด **session ตาม uuid** สำหรับกรณีที่ห้องหายไปแล้วแต่ session ยังไม่ปิด เป็นทางแก้ไม่ใช่วิธีจบสายตามปกติ ```bash curl -X POST "$ME_BASE/voice/calls/$ROOM/hangup" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" -d '{}' ``` ## Go SDK ```go out, err := c.Calls.Hold(ctx, roomName, &sdk.HoldRequest{Muted: true}) _, err = c.Calls.SetMode(ctx, roomName, &sdk.ModeRequest{Mode: "whisper"}) _, err = c.Calls.Hangup(ctx, roomName, nil) ``` --- # ตั้งค่าโทรศัพท์ตั้งโต๊ะ Source: /th/capabilities/desk-phone-provisioning # ตั้งค่าโทรศัพท์ตั้งโต๊ะ โทรศัพท์ตั้งโต๊ะล็อกอินเองไม่ได้ มันทำได้แค่เปิดเครื่อง ถามเครือข่ายว่าไฟล์ตั้งค่าอยู่ที่ไหน ดึงไฟล์นั้นมา แล้วลงทะเบียนตามที่อ่านเจอ ทุกอย่างในหน้านี้มีไว้เพื่อให้การดึงไฟล์ครั้งเดียวนั้น ไปตกที่ไฟล์ที่ถูกต้อง มีสองวิธีในการเปิดใช้งานทั้งชุด ต่างกันแค่ว่าคุณบอกระบบ **ตอนไหน** ว่าเครื่องไหนคู่กับหมายเลขไหน | | ใส่ MAC ล่วงหน้า | ให้เครื่องแจ้งตัวก่อน | |---|---|---| | รู้ MAC ตั้งแต่ก่อนแกะกล่อง | ใช่ | ไม่ | | งานต่อเครื่องหลังแกะกล่อง | ไม่มี | เลือกหมายเลข แล้วสั่งดึงค่าใหม่ | | ลงทะเบียนตั้งแต่เปิดเครื่องครั้งแรก | ใช่ | ไม่ | ทั้งสองวิธีใช้ URL เดียวกัน และใช้ปนกันได้ เครื่องที่ไม่ได้ใส่ MAC ไว้ล่วงหน้าจะไปโผล่ในรายการ เครื่องที่พบเอง ## 1. สร้าง URL ประจำสาขา ไปที่ **ระบบโทรศัพท์ → หมายเลขภายใน → เพิ่มเครื่อง → ค้นหาอัตโนมัติ** เลือกสาขาแล้วสร้างโทเคน จะได้ URL หนึ่งอันสำหรับทั้งสาขา ```text https://<โฮสต์ provisioning ของคุณ>/v1/voice/provision/e// ``` สังเกต `/` ท้าย URL — มันคือไดเรกทอรี ไม่ใช่ไฟล์ ถ้าไม่มี เครื่องจะไปขอ `/.cfg` ซึ่งอยู่นอกไดเรกทอรีของตัวเอง แล้วการดึงครั้งถัดไปจะได้ 404 ทุกครั้ง ::: tip โทเคนนี้ไม่ใช่รหัสผ่าน URL นี้ไม่เคยส่ง SIP password ออกไป คนที่เดาโทเคนถูกจะได้แค่ device row ว่างกับไฟล์ที่บอกว่า "ครั้งต่อไปให้ไปดึงที่นี่" ส่วนข้อมูลจริงจะเสิร์ฟจากไดเรกทอรีของเครื่องนั้นเท่านั้น และต่อเมื่อมี หมายเลขผูกแล้ว URL นี้ติดไว้บนผนังได้ ::: มีสองค่าที่ควรตั้ง: **จำนวนเครื่องสูงสุด** ของสาขา ซึ่งจำกัดความเสียหายถ้าโทเคนหลุด และ **วันหมดอายุ** การเพิกถอนโทเคนไม่กระทบเครื่องที่ลงทะเบียนไปแล้ว ## 2. ชี้ DHCP option 66 มาที่ URL นี้ ### บนเราเตอร์ 3BB เปิด `http://192.168.1.1` (บางรุ่นใช้ `192.168.0.1`) เข้าด้วยบัญชีผู้ดูแลระบบ แล้วหาเมนู DHCP โดยทั่วไปคือ ```text Network → LAN → DHCP Server → DHCP Settings ``` ชื่อเมนูต่างกันตามรุ่น ให้มองหาช่องใดช่องหนึ่งเหล่านี้: **DHCP Option**, **Option 66**, **TFTP Server Name**, **Boot Server**, **Provisioning Server** หรือ **Vendor Specific DHCP Option** | ช่อง | ค่า | |---|---| | Option | 66 | | Type | String / Text | | Value | URL จากขั้นตอนที่ 1 | | Scope | วง DHCP ที่โทรศัพท์อยู่ | ไม่ต้องใส่เครื่องหมายคำพูด และคง `/` ท้าย URL ไว้ บันทึกแล้วรีสตาร์ต DHCP server หรือรีบูตเราเตอร์หนึ่งครั้ง ::: warning เราเตอร์ 3BB หลายรุ่นรับได้แค่ TFTP host เฟิร์มแวร์ที่ผู้ให้บริการล็อกไว้บางรุ่นรับ option 66 เฉพาะชื่อโฮสต์หรือ IP เปล่า ๆ ส่วน URL แบบ `https://` เต็มรูปแบบจะถูกตัดทิ้งเงียบ ๆ ถ้ารีบูตแล้วช่อง **Server URL** ในเครื่องว่างหรือผิด นี่คือสาเหตุที่เป็นไปได้มากที่สุด ใช้ทางเลือกด้านล่างแทนการฝืนกับเฟิร์มแวร์ ::: ### ถ้าเราเตอร์ไม่มี option 66 1. **ใช้เราเตอร์ของเราเอง** ตั้งเครื่อง 3BB เป็น bridge แล้วรัน DHCP เอง 2. **แยก DHCP server ออกมา** เช่น MikroTik, OpenWrt, pfSense, Windows DHCP, dnsmasq หรือ UniFi gateway สำหรับ dnsmasq: ```ini dhcp-option=66,https://<โฮสต์ของคุณ>/v1/voice/provision/e// ``` 3. **ตั้งที่เครื่องโดยตรง** สำหรับ Yealink: **Settings → Auto Provision → Server URL** วิธีนี้ใช้ได้ แต่ต้องเดินไปทีละเครื่อง ซึ่งคือสิ่งที่ option 66 มีไว้เพื่อเลี่ยง ## 3. เลือกวิธีผูกหมายเลข ### ใส่ MAC ล่วงหน้า ที่ **เพิ่มเครื่อง → นำเข้าจาก CSV** อัปโหลดไฟล์ที่มี MAC จากข้างกล่อง ```csv name,extension,mac,vendor,model แผนกต้อนรับ,1001,00:15:65:AA:BB:CC,yealink,yealink-t31w ฝ่ายขาย 1,1002,00:15:65:AA:BB:CD,yealink,yealink-t31w ห้องประชุม,1003,,poly,poly-vvx350 ``` - `name`, `vendor`, `model` ต้องมี ส่วน `extension` ต้องเป็นหมายเลขที่มีอยู่จริง - `mac` ใส่หรือไม่ใส่ก็ได้ในแต่ละแถว แถวที่สามด้านบนไม่ได้ใส่ เครื่องนั้นจะไปเข้าทางแจ้งตัวเองแทน - ใช้ตัวคั่นแบบไหนก็ได้ (`00:15:65:AA:BB:CC`, `001565aabbcc`, `00-15-65-aa-bb-cc`, `0015.65aa.bbcc`) หน้าตรวจสอบจะแสดงรูปแบบที่จะถูกบันทึกจริง - MAC ที่ผูกกับเครื่องอื่นไปแล้วจะถูกปฏิเสธตั้งแต่ตอนอัปโหลด ไม่ใช่ตอนเปิดเครื่อง เสียบสายแล้วจบ แต่ละเครื่องจะขอไฟล์ `.cfg` ของตัวเอง ระบบรู้จัก แล้วลงทะเบียนทันที ### ให้เครื่องแจ้งตัวก่อน เสียบสายก่อนเลย เครื่องที่ระบบยังไม่รู้จักจะขึ้นในรายการภายในไม่กี่นาที พร้อม MAC และรหัสสั้น ๆ สำหรับยืนยัน จากนั้นเลือกหมายเลขให้แต่ละแถวแล้วกด **กำหนดหมายเลข** หรือใช้ **ยืนยันด้วยรหัส** เพื่อผูกทีละเครื่อง ::: warning การกำหนดหมายเลขยังไม่ถึงตัวเครื่องทันที เครื่องจะรับค่าใหม่ในรอบตรวจถัดไป ซึ่งอาจอีกหลายชั่วโมง ถ้าต้องการให้มีผลเดี๋ยวนั้น ให้รีบูต หรือกด **Autoprovision Now** ที่เครื่อง ::: ## 4. ตรวจผล ที่เครื่อง **Status** จะบอก MAC และ IP ส่วน **Settings → Auto Provision** จะแสดง Server URL ที่ได้รับจาก DHCP เมื่อตั้งค่าสำเร็จ **Account → Register Status** ต้องขึ้น `Registered` และดูรายละเอียดการดึงไฟล์ได้ที่ **Status → System Log** ในแดชบอร์ด **ระบบโทรศัพท์ → หมายเลขภายใน → กิจกรรม** บันทึกการดึงไฟล์และการตรวจสอบการลงทะเบียน ทุกครั้ง รวมถึงครั้งที่ล้มเหลว เช่น ไฟล์ที่เสิร์ฟออกไปโดยยังไม่มีหมายเลขผูก, MAC ที่ถูกปฏิเสธเพราะ เป็นขององค์กรอื่น หรือการลงทะเบียนของหมายเลขที่ไม่มีเครื่องไหนถือครอง ::: tip เครื่องที่ขึ้น localhost คือเครื่องที่ชี้กลับหาตัวเอง ถ้า Server URL ในเครื่องเป็น `http://localhost:3003` หรือ `127.0.0.1` เครื่องจะแปลค่านั้น เป็นตัวมันเองและไปไม่ถึงระบบ โฮสต์ provisioning ต้องเป็นชื่อที่เครื่องแปลงได้จากเครือข่ายของมันเอง ::: ## อ้างอิง จัดการโทเคนประจำสาขาผ่าน API ที่ผูกกับองค์กร path ด้านล่างนับจาก API base ที่ระบุไว้ใน [เอกสารอ้างอิง API](/th/api-reference) ส่วน `organizationId` คือค่าเดียวกับที่ส่งใน `X-TenantID` | Method | Path | |---|---| | `GET` | `/organization/{organizationId}/voice/enrollment-tokens` | | `POST` | `/organization/{organizationId}/voice/enrollment-tokens` | | `DELETE` | `/organization/{organizationId}/voice/enrollment-tokens/{uuid}` | `POST` รับ `org_node_uuid` และจะใส่ `label`, `max_devices` (`0` = ไม่จำกัด), `expires_at` ด้วยก็ได้ เส้นทางฝั่งโทรศัพท์ที่ URL ของ option 66 ชี้ไป **ไม่ผ่านการยืนยันตัวตนโดยตั้งใจ** เพราะเครื่องที่ กำลังบูตไม่มีอะไรจะยืนยัน จึงไม่ได้ประกาศไว้เป็น operation ที่เรียกได้ในเอกสารนี้ เส้นทางนี้จำกัด อัตราการเรียกต่อ client และปฏิเสธ MAC ขององค์กรอื่นด้วย `404` ตัวเดียวกับที่โทเคนผิดได้รับ คนที่ไล่สแกนจึงแยกสองกรณีนี้ไม่ออก --- # โทรออก Source: /th/capabilities/place-calls # โทรออก การโทรแต่ละแบบมี endpoint ของตัวเอง เลือกตามสิ่งที่ต้องการให้เกิดขึ้นเมื่อปลายสายรับ | แบบ | Route | เมื่อปลายสายรับ | ผลตอบกลับ | | ---------- | -------------------------------- | -------------------------------------------- | --------------- | | **Dial** | `POST /telephony/call/dial` | ไม่มีเสียงเล่น ใช้สายนี้เพื่อพักหรือโอนต่อ | `202` queued | | **TTS** | `POST /telephony/call/tts` | อ่านข้อความ แล้ววางสาย | `202` queued | | **Agent** | `POST /telephony/call/agent` | AI agent คุยกับปลายสาย | `201` ringing | | **Survey** | `POST /telephony/call/survey` | ถามคำถามและส่งคำตอบกลับมาให้คุณ | `202` หรือ `201` | | **Flow** | `POST /telephony/call/flow` | ยังใช้ไม่ได้ — สายดัง แต่ยังไม่รัน flow | `201` ringing | | **Batch** | `POST /telephony/call/batch` | โทรแบบ TTS หรือ dial หนึ่งสายต่อหนึ่งเบอร์ | `202` queued | ติดตามทุกสายได้ด้วย `GET /telephony/call/{call_uuid}` API key ต้องมีสิทธิ์ **`voice.originate`** ส่วนการโทรหาเบอร์ต่อภายใน (`POST /telephony/call/extension`) ต้องใช้ผู้ใช้ที่ลงชื่อเข้าใช้ เพราะ API key ไม่มีเครื่องโทรศัพท์ให้ดัง ::: warning แต่ละ endpoint รับเฉพาะฟิลด์ของตัวเอง ฟิลด์ของการโทรแบบอื่นจะถูกปฏิเสธด้วย `400` พร้อมบอก endpoint ที่ถูกต้อง เช่น `survey belongs to POST /telephony/call/survey` `POST /telephony/call` แบบเดิมยังใช้กับ dial, TTS, agent, flow และ extension ได้ แต่เลิกแนะนำแล้ว และทำแบบสำรวจไม่ได้ ::: ## โทรหาใคร และโทรจากเบอร์ไหน การโทรหาเบอร์ทุกแบบใช้ฟิลด์ปลายทางชุดเดียวกัน | ฟิลด์ | ชนิด | หมายเหตุ | | ----------------------- | ------ | ----------------------------------------------------------- | | `destination` | string | จำเป็น เบอร์ที่จะโทร ในรูปแบบ E.164 (`+66812345678`) | | `caller_id` | string | ไม่บังคับ เบอร์ของคุณที่จะแสดงให้ปลายสายเห็น | | `caller_identity_id` | string | ไม่บังคับ ใช้ caller identity ที่บันทึกไว้แทน `caller_id` | | `caller_extension_uuid` | string | ไม่บังคับ โทรออกในนามเบอร์ของ extension นี้ | | `trunk` | string | ไม่บังคับ บังคับใช้ trunk ที่ระบุ | | `organization_id` | string | ใช้เมื่อเรียกด้วย token ของผู้ใช้เท่านั้น API key ระบุองค์กรอยู่แล้ว | Dial, TTS และ batch รับ `mode` ได้ด้วย: `"queue"` (ค่าเริ่มต้น) รอจนมีสายว่าง ส่วน `"instant"` โทรทันที หรือตอบ `429` เมื่อไม่มีสายว่าง ## ลองใหม่อย่างปลอดภัยด้วย `Idempotency-Key` ส่ง header `Idempotency-Key` ทุกครั้งที่โทรออก หากส่งซ้ำด้วย key เดิม ระบบจะคืนสายแรกแทนการโทรหาปลายสายซ้ำ SDK ทางการส่ง key ให้อัตโนมัติ และใช้ key เดิมเมื่อ SDK ลองใหม่เอง ```bash curl -X POST "$ME_BASE/telephony/call/tts" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Idempotency-Key: order-1042-reminder" \ -H "Content-Type: application/json" \ -d '{ "destination": "+66812345678", "tts": { "text": "คำสั่งซื้อ 1042 กำลังนำส่ง", "voice_id": "eve", "language": "th" } }' ``` ```json { "kind": "pstn", "call": { "call_uuid": "5d2c…", "destination": "+66812345678", "status": "queued" } } ``` `tts` รับ `text` และ `voice_id` และรับ `provider`, `language` และ `repeat` (จำนวนครั้งที่เล่น `0` คือเล่นครั้งเดียว) ได้ด้วย ## ต่อสายให้ AI agent ```json { "destination": "+66812345678", "agent": { "id": "agent_support_th" }, "gather": { "parameters": { "type": "object", "properties": { "order_id": { "type": "string" } }, "required": ["order_id"] }, "callback_url": "https://example.com/hooks/order" } } ``` `agent.id` คือ agent ที่บันทึกไว้ (ใส่ `config_mode: "draft"` เพื่อใช้ฉบับร่าง) ส่วน `system_prompt`, `voice` และ `language` ใช้แทนค่าของ agent เฉพาะสายนี้ `gather` ไม่บังคับ agent จะถามข้อมูลตาม `parameters` แล้วส่งคำตอบกลับมา ดู [รับคำตอบ](#รับคำตอบ) ## ทำแบบสำรวจ แบบสำรวจจะโทรหาเบอร์ ถามคำถามของคุณ แล้วส่งคำตอบกลับมา ทำได้สองแบบ ### กดปุ่ม (`"mode": "dtmf"`) ระบบอ่านคำถามทีละข้อ แล้วปลายสายตอบด้วยการกดปุ่มหนึ่งปุ่ม ```json { "destination": "+66812345678", "survey": { "mode": "dtmf", "survey_id": "csat_q3", "greeting": "ขอบคุณที่ใช้บริการค่ะ", "questions": [ { "key": "score", "prompt": "พึงพอใจแค่ไหน กด 1 ถึง 5", "min": 1, "max": 5 }, { "key": "would_return", "prompt": "จะกลับมาใช้บริการอีกไหม กด 1 ไม่ กด 2 ใช่", "min": 1, "max": 2, "required": false } ], "retry_prompt": "กดได้ตั้งแต่ {min} ถึง {max} กดใหม่อีกครั้ง", "closing": "ขอบคุณค่ะ สวัสดีค่ะ", "tts": { "provider": "xai", "voice_id": "eve", "language": "th" }, "callback_url": "https://example.com/hooks/survey" } } ``` | ฟิลด์ | หมายเหตุ | | -------------- | -------------------------------------------------------------------------------------- | | `questions` | จำเป็น สูงสุด 20 ข้อ ถามตามลำดับ แต่ละข้อรับปุ่มเดียวระหว่าง `min` ถึง `max` (0–9) | | `key` | ตัวพิมพ์เล็ก ตัวเลข และ `_` ขึ้นต้นด้วยตัวอักษร ใช้เป็นชื่อของคำตอบ | | `required` | ค่าเริ่มต้น `true` ถ้าข้อบังคับไม่ได้รับคำตอบ สายจะจบ ส่วนข้อไม่บังคับจะข้ามไป | | `retry_prompt` | พูดก่อนถามซ้ำ เมื่อกดปุ่มนอกช่วงของข้อนั้น โดยเติม `{min}` และ `{max}` ให้ ถ้าไม่ระบุจะถามคำถามซ้ำเลย ปลายสายกดได้ 3 ครั้ง | | `survey_id` | จัดกลุ่มคำตอบในรายงานแบบสำรวจ คำตอบแต่ละข้อบันทึกเป็น `.` เช่น `csat_q3.score` แต่ละข้อจึงคงสเกลของตัวเอง ค่าเริ่มต้น `telephony_survey` | | `tts` | เสียงที่ใช้อ่านทุกข้อความ: `provider`, `voice_id`, `language`, `instructions` ผู้ให้บริการที่ใช้ไม่ได้จะถูกปฏิเสธด้วย `400` ก่อนเข้าคิว | แบบสำรวจแบบกดปุ่มจะเข้าคิว ผลตอบกลับคือ `202` พร้อม `status: "queued"` ::: tip ให้คำตอบที่ดีกว่าอยู่บนปุ่มที่สูงกว่า รายงานแบบสำรวจนับว่าพึงพอใจเมื่อคะแนนอยู่ในสองอันดับบนของสเกล สำหรับคำถามใช่/ไม่ ให้ **ใช่** อยู่บนปุ่มที่สูงกว่า (`1` ไม่ `2` ใช่) รายงานจะอ่านได้ตรงตามที่คาด ::: ### พูดตอบ (`"mode": "agent"` ค่าเริ่มต้น) AI agent ถามข้อมูลตาม `parameters` ด้วยเสียง และวางสายเมื่อได้คำตอบที่จำเป็นครบและถูกต้อง สายจะดังทันที ผลตอบกลับคือ `201` ```json { "destination": "+66812345678", "survey": { "greeting": "สวัสดีค่ะ ขอเวลาทำแบบสำรวจสั้น ๆ", "parameters": { "type": "object", "properties": { "rating": { "type": "integer", "x-parse": "integer" }, "comment": { "type": "string" } }, "required": ["rating"] }, "language": "th", "callback_url": "https://example.com/hooks/survey" } } ``` `x-parse` กำหนดรูปแบบคำตอบที่ agent ส่งกลับของแต่ละฟิลด์: `text`, `integer`, `number`, `boolean`, `date`, `phone` หรือ `email` ฟิลด์ของแบบกดปุ่ม (`questions`, `closing`, `retry_prompt`, `survey_id`, `tts`) จะถูกปฏิเสธในโหมดนี้ ## รับคำตอบ คำตอบจาก gather หรือแบบสำรวจส่งถึงคุณสองทาง ด้วย body เดียวกัน - **Webhook** — event `call.ai_gather.ended` ใน webhook ของ workspace ส่งเมื่อสายที่มีคนรับจบลง พร้อม `status` เป็น `valid` หรือ `incomplete` - **`callback_url`** — ส่งเฉพาะเมื่อคำตอบที่จำเป็นครบและ `valid` ทั้งหมด พร้อมลายเซ็น: `X-webhook-signature` คือ HMAC-SHA256 แบบ base64 ของ body ดิบ โดยใช้ `call.gather.callback_secret` จากผลตอบกลับตอนโทร (หรือ `callback_secret` ที่คุณส่งมา) เป็น key ```json { "event": "call.ai_gather.ended", "id": "evt_…", "created_at": "2026-09-25T09:30:12Z", "workspace_id": "org_…", "gather_id": "…", "session_uuid": "…", "status": "valid", "result": { "score": 4, "would_return": 2 } } ``` จับคู่ `gather_id` กับ `call.gather.gather_id` จากผลตอบกลับตอนโทร ## โทรหลายเบอร์ ```json { "destinations": ["+66811111111", "+66822222222"], "tts": { "text": "วันนี้สำนักงานปิดทำการ", "voice_id": "eve", "language": "th" } } ``` ```json { "queued": 2, "total": 2, "calls": [{ "destination": "+66811111111", "call": { "call_uuid": "…", "status": "queued" } }] } ``` เบอร์ที่เข้าคิวไม่ได้จะมี `error` ในรายการของเบอร์นั้น เบอร์อื่นยังโทรออกตามปกติ ## สถานะของสาย ```bash curl "$ME_BASE/telephony/call/$CALL_UUID" \ -H "API-Key: $ME_API_KEY" -H "X-TenantID: $ME_TENANT" ``` ```json { "call_uuid": "5d2c…", "room": "…", "destination": "+66812345678", "status": "completed", "handled_by_ai": false, "duration_secs": 42 } ``` `status` เปลี่ยนจาก `pending`, `ringing` และ `in_progress` แล้วจบที่ `completed`, `no_answer`, `failed` หรือ `canceled` ## SDK ```go res, err := c.Telephony.Survey(ctx, &sdk.SurveyCallRequest{ DialTarget: sdk.DialTarget{Destination: "+66812345678"}, Survey: &sdk.Survey{ Mode: sdk.SurveyModeDTMF, SurveyID: "csat_q3", Questions: []sdk.SurveyQuestion{{Key: "score", Prompt: "กด 1 ถึง 5", Min: 1, Max: 5}}, }, }) ``` ```ts const res = await me.telephony.survey({ destination: "+66812345678", survey: { mode: "dtmf", survey_id: "csat_q3", questions: [{ key: "score", prompt: "กด 1 ถึง 5", min: 1, max: 5 }] }, }); ``` SDK ทุกภาษามีบริการ `telephony` เหมือนกัน: `dial`, `tts`, `agent`, `survey`, `flow`, `extension`, `batch` และ `status` --- # ชื่อผู้ส่ง Source: /th/capabilities/sender-names # ชื่อผู้ส่ง ชื่อผู้ส่งคือชื่อที่ผู้รับเห็นเป็นต้นทางของข้อความ ผู้ให้บริการกำหนดให้ต้องลงทะเบียนและ ได้รับอนุมัติก่อนจึงจะส่งได้ การลงทะเบียนจึงเป็นการยื่นเอกสาร KYC ไม่ใช่แค่ตั้งชื่อ ทุก route อยู่ในขอบเขตองค์กร และยืนยันตัวตนคนละแบบกับ API ฝั่งส่งข้อความ คือใช้ token ของผู้ใช้ที่ล็อกอินอยู่ ไม่ใช่ API key — ถ้าส่ง `API-Key` มาจะถูกปฏิเสธด้วย `401` เพราะ route กลุ่มนี้ตรวจสิทธิ์จากการเป็นสมาชิกของ `{organizationId}` ซึ่งอ่านจาก path จึงไม่ต้องส่ง `X-TenantID` ด้วย | การทำงาน | Route | | --------------------- | ----------------------------------------------------------------- | | ดูรายการ | `GET /organization/{organizationId}/sender-names` | | ตรวจสอบว่าว่างหรือไม่ | `GET /organization/{organizationId}/sender-names/availability` | | ลงทะเบียน | `POST /organization/{organizationId}/sender-names` | | แก้ไขเอกสาร | `PUT /organization/{organizationId}/sender-names/{documentId}` | | ลบ | `DELETE /organization/{organizationId}/sender-names/{documentId}` | ## ตรวจสอบก่อนว่าชื่อว่างหรือไม่ ```bash curl "$ME_BASE/organization/$ME_TENANT/sender-names/availability?sender_name=MyBrand" \ -H "Authorization: Bearer $ME_ACCESS_TOKEN" ``` ## ลงทะเบียน การลงทะเบียนคือการยื่น KYC เพราะผู้ให้บริการต้องรู้ว่าใครอยู่เบื้องหลังชื่อนั้น ไม่ใช่แค่ชื่ออะไร ```bash curl -X POST "$ME_BASE/organization/$ME_TENANT/sender-names" \ -H "Authorization: Bearer $ME_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "sender_name": "MyBrand", "usage_type": "Transactional", "business_name": "My Company Co., Ltd.", "contact_person": "มาลี สุขใจ", "country": "TH", "email": "ops@example.com", "name": "มาลี สุขใจ", "nationality": "TH", "phone_number": "+66886578553", "purpose": "แจ้งสถานะคำสั่งซื้อและการจัดส่ง", "company_registration_certificate": "https://…/cert.pdf", "brand_ownership_proof": "https://…/trademark.pdf" }' ``` ## การแก้ไข `PUT` เป็นการแก้ไขเอกสารแบบบางส่วน มีสี่ฟิลด์ที่แก้ไม่ได้โดยตั้งใจ | ฟิลด์ | เหตุผล | | -------------------- | -------------------------------------- | | `sender_name` | เปลี่ยนชื่อต้องผ่าน KYC ใหม่ | | `obj_status` | ระบบกำหนด เป็นผลการพิจารณา | | `is_active` | ระบบกำหนด | | `approved_operators` | กำหนดโดยผู้ให้บริการที่อนุมัติชื่อนั้น | หากต้องการเปลี่ยนชื่อ ให้ลงทะเบียนชื่อใหม่ ::: tip การอนุมัติแยกตามผู้ให้บริการ `approved_operators` คือรายชื่อผู้ให้บริการที่อนุมัติชื่อนี้แล้ว การส่งด้วยชื่อที่ยังไม่ได้รับอนุมัติ สำหรับผู้ให้บริการปลายทางจะไม่ทำให้ทั้งคำขอล้มเหลว แต่ผู้รับเหล่านั้นจะถูกนับใน `rejected` ดู [การส่ง SMS](/th/capabilities/sms) ::: --- # การส่ง SMS Source: /th/capabilities/sms # การส่ง SMS มีสองพื้นผิวสำหรับการส่ง และใช้แทนกันไม่ได้ | ประเภท | Route | ใช้สำหรับ | | ------------- | --------------------------------- | ------------------------------------ | | **Message** | `POST /sms/send_messages/message` | ทรานแซคชัน หนึ่งแถวต่อผู้รับหนึ่งราย | | **Broadcast** | `POST /sms/broadcast` | ข้อความเดียวส่งไปยังผู้รับจำนวนมาก | ::: warning Broadcast ต้องเปิดใช้ที่ฝั่ง deployment `/sms/broadcast` จะไม่ถูกเปิดใช้ เว้นแต่ deployment ตั้งค่า `ME_SMS_PRODUCER_ENABLED` สภาพแวดล้อมที่ไม่ได้เปิดจะตอบ `404` ซึ่งไม่ใช่ข้อความผิดพลาดที่บอกสาเหตุ ควรตรวจสอบกับผู้ดูแลระบบก่อนเริ่มพัฒนา ::: ## ส่งข้อความ แต่ละแถวมีผู้ส่ง ผู้ให้บริการ ผู้รับ และเนื้อหาของตัวเอง แถวที่ใช้ operator + ชื่อผู้ส่ง + ข้อความเดียวกันจะถูกรวมเป็น **หนึ่ง batch** ```json { "check_spam_msg_mode": "not_check", "data": [ { "sender_name": "MyBrand", "operator": "ais", "phone_number": "+66886578553", "message": "คำสั่งซื้อของคุณจัดส่งแล้ว" } ] } ``` - **`sender_name`** ต้อง[ได้รับอนุมัติ](/th/capabilities/sender-names)สำหรับผู้ให้บริการนั้น - **`operator`** คือรหัสผู้ให้บริการ (`ais`, `dtac`, `true_corp`) - **`phone_number`** รูปแบบ E.164 - **`timeout_seconds`** รับไว้เพื่อความเข้ากันได้ แต่ไม่มีผล เพราะ timeout เป็นนโยบายของ gateway ผลลัพธ์ห่อไว้ใน `data` ```json { "data": { "accepted": 1, "rejected": 0, "batches": [ { "batch_id": "018f7b2a-7c3e-7a1b-9c2d-1a2b3c4d5e6f", "accepted": 1, "rejected": 0 } ] } } ``` `accepted` คือจำนวนผู้รับที่เข้าคิวส่งไปยังผู้ให้บริการแล้ว ส่วน `rejected` คือผู้รับที่ถูกตัดออก ก่อนเข้าคิว เช่นหมายเลขว่างหรือซ้ำ หรือเกิดข้อผิดพลาดตอนส่งเข้าคิว หมายเลขเสียหนึ่งรายการ จะไม่ทำให้ทั้งคำขอล้มเหลว ## Broadcast หนึ่งข้อความ หนึ่งผู้ให้บริการ หนึ่งชื่อผู้ส่ง ส่งไปยังผู้รับหลายราย ```bash curl -X POST "$ME_BASE/sms/broadcast" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "operator": "ais", "sender_name": "MyBrand", "message": "ลดราคาถึงคืนนี้", "recipients": ["+66886578553", "+66886578554"], "idempotency_key": "campaign-2026-09-12" }' ``` | ฟิลด์ | หมายเหตุ | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | `idempotency_key` | ป้องกันการส่งซ้ำ หากส่งคำขอเดิมด้วย key เดิมจะได้ผลลัพธ์ครั้งแรกกลับมา แทนการส่งใหม่ | | `validity_seconds` | ขอให้**ผู้ให้บริการ**ทิ้งข้อความหากส่งไม่สำเร็จภายในช่วงเวลานี้ DTAC และ True รองรับ ส่วน AIS ไม่มีฟิลด์นี้ใน MT XML จึงถูกละไว้ | จากนั้นตรวจสถานะของ batch ```bash curl "$ME_BASE/sms/broadcast/$BATCH_ID" \ -H "API-Key: $ME_API_KEY" -H "X-TenantID: $ME_TENANT" ``` ```json { "batch_id": "…", "total": 2, "delivered": 1, "sent": 1, "failed": 0, "timeout": 0, "pending": 0 } ``` ## `undeliverable_chars` batch อาจส่งค่า `undeliverable_chars` กลับมา ซึ่งคือตัวอักษรที่ผู้ให้บริการที่นำส่ง batch นั้น **ทำหายแน่นอน** แสดงเป็น `"U+1F381 U+1D7EE"` ค่านี้มีอยู่เพราะความเสียหายมองไม่เห็นจากภายนอก ผู้ให้บริการรับข้อความไว้ คู่ surrogate ถูกคิดเป็นสองหน่วย แต่เครื่องปลายทางไม่แสดงอะไรตรงนั้น หากพบค่านี้ ให้เปลี่ยนไปใช้ ตัวอักษรที่ส่งผ่านได้ ::: warning Endpoint SMS ชุดเดิม เอกสารรุ่นก่อนเคยอธิบาย `/sms/send_messages/{otp,broadcast,eagleeye,cancel}` ซึ่ง **ไม่ได้อยู่ใน API ปัจจุบัน** การตรวจสอบ OTP ย้ายไปที่ [ยืนยันตัวตน / OTP](/th/capabilities/verify) ส่วน broadcast คือ `/sms/broadcast` ด้านบน ::: --- # ยืนยันตัวตน / OTP Source: /th/capabilities/verify # ยืนยันตัวตน / OTP มีสองแบบ ยืนยันด้วย API key ทั้งคู่ **SMS OTP** ดูแลครบวงจร ตั้งแต่สร้างรหัส ส่ง ไปจนถึงตรวจสอบ | การทำงาน | Route | | ------------- | ------------------------------ | | ส่งรหัส | `POST /sms/otp` | | ตรวจสอบรหัส | `POST /sms/otp/{otpId}/verify` | | ส่งรหัสอีกครั้ง | `POST /sms/otp/{otpId}/resend` | | อ่านสถานะ | `GET /sms/otp/{otpId}` | **ตรวจสอบอย่างเดียว** ใช้กับรหัสที่ระบบสร้างจากที่อื่น เช่นตอนสมัครสมาชิก โดยอ้างอิงด้วย `ref_code` ไม่ใช่ session id | การทำงาน | Route | | ----------- | ------------------ | | ตรวจสอบรหัส | `POST /otp/verify` | ::: tip เลือกแบบไหน ถ้าเริ่มจากศูนย์ ใช้ `POST /sms/otp` ส่วน `/otp/verify` ใช้เมื่อมีระบบอื่นสร้างรหัสไว้แล้ว และส่ง `ref_code` มาให้ — รหัสที่คุณสร้างเองและส่งผ่าน SMS ของคุณเอง ตรวจสอบที่นั่นไม่ได้ เพราะระบบหาไม่เจอ ::: ## ส่ง OTP ```bash curl -X POST "$ME_BASE/sms/otp" \ -H "API-Key: $ME_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "mode": "auto", "operator": "smart", "sender_name": "YourBrand", "recipient": "+66886578553", "config": { "length": 6, "type": "numeric", "validity_seconds": 300 } }' ``` ```json { "id": "0f3c…", "mode": "auto", "recipient": "+66886578553", "ref_code": "A1B2", "expires_at": "2026-09-19T04:21:00Z", "status": "sent", "message_id": "tx-…" } ``` เก็บ `id` ไว้ ใช้อ้างอิงตอนตรวจสอบ ส่งซ้ำ และอ่านสถานะ ส่วนที่แสดงให้ผู้ใช้เห็นคือ `ref_code` — **ตัวรหัสจะไม่ถูกส่งกลับมา** ระบบเก็บไว้เป็นค่าแฮชเท่านั้น จึงไม่มีทางดึงกลับได้ | ฟิลด์ | หมายเหตุ | | --------------------- | --------------------------------------------------------------- | | `mode` | `auto` (ระบบสร้างและตรวจสอบให้) หรือ `provided` (คุณสร้างเอง ระบบส่งให้อย่างเดียว) | | `operator` | `ais`, `dtac`, `true_corp` หรือ `smart` ให้ระบบเลือกเส้นทางให้ | | `sender_name` | ชื่อผู้ส่งที่องค์กรคุณได้รับอนุมัติ ดูรายละเอียดด้านล่าง | | `recipient` | รูปแบบ E.164 | | `message_template` | ต้องมี `{{otp}}` ถ้าไม่ส่งมา ระบบใช้ข้อความมาตรฐาน | | `message_template_id` | id ของเทมเพลตที่คุณบันทึกไว้ ใช้พร้อม `message_template` ไม่ได้ | | `config` | เฉพาะ `auto` — `length` 4–10, `type`, `validity_seconds` 1–3600 | | `otp` / `ref_code` | เฉพาะ `provided` — รหัสและเลขอ้างอิงของคุณเอง | `sender_name` ใช้ข้อตกลงเดียวกับ [SMS](/th/capabilities/sms) ชื่อที่ยังไม่ได้รับอนุมัติ จะถูกปฏิเสธด้วย `403` ไม่ใช่เปลี่ยนให้เงียบๆ ดูรายชื่อที่ใช้ได้จาก `GET /organization/{organizationId}/sender-names` — route นั้นใช้ token ของผู้ใช้ ไม่ใช่ `API-Key` ดู [ชื่อผู้ส่ง](/th/capabilities/sender-names) ในรายการนั้นจะมีชื่อกลางของแพลตฟอร์มรวมอยู่ด้วย สังเกตจาก `is_platform_default: true` ซึ่งทุกองค์กรส่งได้โดยไม่ต้องขออนุมัติเอง แต่ชื่อนี้มีเฉพาะเมื่อผู้ดูแลระบบตั้งค่าไว้ จึงควรอ่านจากรายการจริง ไม่ใช่สมมติว่ามี ### เทมเพลตข้อความ `{{otp}}` เป็นตัวบังคับ ข้อความที่ไม่มีตัวนี้จะคิดค่าส่ง SMS ทั้งที่ผู้ใช้ทำอะไรต่อไม่ได้ ส่วน `{{ref_code}}` ใส่หรือไม่ใส่ก็ได้ ```jsonc // เขียนตรงๆ { "message_template": "{{otp}} คือรหัส OTP ของคุณ Ref: {{ref_code}}" } // หรืออ้างด้วย id จาก /template-message/organization/{organizationId} { "message_template_id": "8f1c…" } ``` ส่งมา**ทั้งคู่**จะถูกปฏิเสธด้วย `400` เพราะค่าเดียวที่มีสองแหล่งทำให้ไม่รู้ว่าคุณตั้งใจแบบไหน และการเดาจะส่งข้อความที่คุณไม่ได้เขียนออกไป id ของเทมเพลตจะถูกแปลงเป็น**ข้อความ**ก่อนประกอบเป็น SMS การแก้เทมเพลตทีหลัง จึงไม่กระทบข้อความที่ส่งไปแล้ว รวมถึงการส่งซ้ำ ซึ่งใช้ข้อความ ณ ตอนส่งครั้งแรก ### ส่งซ้ำอย่างปลอดภัย ใส่ `Idempotency-Key` แล้วการยิงซ้ำจะได้ OTP เดิมกลับมา ไม่ใช่รหัสใหม่ การอ้างเทมเพลตด้วย id กับการพิมพ์ข้อความเดียวกันตรงๆ นับเป็นคำขอ*เดียวกัน* เพราะ key ครอบข้อความที่แปลงแล้ว ถ้าใช้ key เดิมกับเนื้อหาที่ต่างกันจริงจะถูกปฏิเสธ — การคืนผลลัพธ์เดิมในกรณีนั้น เท่ากับบอกว่ามีรหัสถูกส่งออกไปแล้วทั้งที่ไม่เคยส่ง ## ตรวจสอบ OTP จาก SMS OTP ```bash curl -X POST "$ME_BASE/sms/otp/$OTP_ID/verify" \ -H "API-Key: $ME_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "otp": "884213" }' ``` ```json { "id": "0f3c…", "verified": false, "status": "pending", "attempts_remaining": 2 } ``` ::: warning รหัสผิดได้ `200` ไม่ใช่ `4xx` คำขอถูกต้องดีอยู่ แค่รหัสผิดเท่านั้น ให้ดูที่ `verified` ไม่ใช่ status code ::: `attempts_remaining` จะ**หายไป**เมื่อ session ถูกล็อก ซึ่งไม่เท่ากับ `0` หน้าจอที่แสดง "เหลือ N ครั้ง" ควรไม่แสดงอะไรเลยในกรณีนั้น ## ตรวจสอบรหัสที่สร้างจากที่อื่น ```bash curl -X POST "$ME_BASE/otp/verify" \ -H "API-Key: $ME_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone_number": "+66886578553", "code": "884213", "ref_code": "A1B2", "purpose": "signup" }' ``` | ฟิลด์ | หมายเหตุ | | -------------- | -------------------------------------------------- | | `phone_number` | รูปแบบ E.164 | | `code` | รหัสที่ผู้ใช้กรอก | | `ref_code` | รหัสอ้างอิงที่ได้ตอนสร้างรหัส | | `email` | ไม่บังคับ | | `purpose` | ขอบเขตการใช้สิทธิ์ เช่น `signup`, `welcome_credit` | ```json { "is_valid": true, "status": "verified", "message": "OK", "is_new": true } ``` ## `is_new` อาจไม่มีค่า ซึ่งไม่เท่ากับ `false` `is_new` ตอบคำถามว่า "หมายเลขนี้ใช้สิทธิ์สำหรับ `purpose` นี้เป็นครั้งแรกหรือไม่" ค่านี้จะ**ไม่ถูกส่งมา**เมื่อไม่มีอะไรให้ตอบ คือไม่ได้ส่ง `purpose` มา หรือระบบบันทึกสิทธิ์ติดต่อไม่ได้ ความต่างนี้สำคัญมากถ้าคุณใช้ค่านี้ตัดสินการให้สิทธิ์ ```ts // ถูกต้อง: ถ้าไม่มีค่า ให้ระงับไว้ ไม่ใช่จ่ายสิทธิ์ทั้งที่ยังไม่รู้ if (res.is_new === true) grantWelcomeCredit(); ``` การถือว่าไม่มีค่าเท่ากับ `false` นั้นปลอดภัย แต่ถ้าเขียนกลับด้าน (`if (!res.is_new)`) จะกลายเป็นจ่ายสิทธิ์ทุกครั้งที่เกิดข้อผิดพลาด แทนที่จะไม่จ่ายเลย ## API key เป็นตัวระบุโปรเจกต์ Key จะ resolve ไปยังโปรเจกต์เดียว และการใช้สิทธิ์จะถูกบันทึกกับโปรเจกต์นั้น key ที่ resolve ไม่ได้จะถูกปฏิเสธด้วย `401` — ดู [การยืนยันตัวตน](/th/overview/authentication) --- # เสียง (Voice) Source: /th/capabilities/voice # เสียง (Voice) งานด้านเสียงแยกเป็นสามหน้า ตามสิ่งที่คุณกำลังทำ | ต้องการ | ไปที่ | | ----------------------------------- | ------------------------------------------------ | | **โทรออก** — dial ข้อความ agent หรือแบบสำรวจ | [โทรออก](/th/capabilities/place-calls) | | ควบคุมสายที่**กำลังสนทนาอยู่** | [ควบคุมสายที่กำลังสนทนา](/th/capabilities/calls) | | ตั้งค่า **agent** หรือดูรายการเสียง | [Voice Agent](/th/capabilities/agents) | ## การโทรออก การโทรแต่ละแบบมี endpoint ของตัวเองใต้ `/telephony/call/…` — ให้เบอร์ดัง อ่านข้อความ ต่อ AI agent หรือทำแบบสำรวจแบบกดปุ่มหรือพูดตอบ ส่ง `Idempotency-Key` เพื่อไม่ให้การลองใหม่โทรซ้ำ → [โทรออก](/th/capabilities/place-calls) ## การควบคุมสาย สายที่กำลังสนทนาอ้างอิงด้วย **ชื่อห้อง (room name) ของ LiveKit** ไม่ใช่ session id เพราะสิ่งที่มีอยู่จริงขณะที่ทั้งสองฝ่ายยังคุยกันคือห้อง การพักสาย โหมดหัวหน้างาน การวางสาย การล็อกห้อง และการนำผู้เข้าร่วมออก อยู่ที่หน้านั้น → [ควบคุมสายที่กำลังสนทนา](/th/capabilities/calls) ## Agent และรายการเสียง การตั้งค่า agent กำหนดพฤติกรรมของ agent ส่วนรายการเสียงบอกว่า deployment นี้ ใช้ผู้ให้บริการและเสียงใดได้จริง ซึ่งขึ้นกับ vendor key ที่ตั้งค่าไว้ จึงควรอ่านตอนใช้งานจริง แทนการเขียนรายชื่อผู้ให้บริการไว้ในโค้ด → [Voice Agent](/th/capabilities/agents) ::: warning Endpoint เสียงชุดเดิม เอกสารรุ่นก่อนเคยอธิบาย `/voice/otp`, `/voice/broadcast`, `/voice/dial`, `/voice/sessions` และ `/voice/ivr_config` ซึ่ง **ไม่ได้อยู่ใน API ปัจจุบัน** และจะตอบ `404` หากคุณเคยพัฒนากับ endpoint เหล่านั้น ให้ใช้หน้าด้านบนแทน ::: --- # การส่ง Webhook Source: /th/capabilities/webhook-deliveries # การส่ง Webhook หน้านี้คือฝั่ง **ขาออก** ของ webhook คือสิ่งที่ MessageEagle พยายามส่งให้คุณ และ endpoint ของคุณตอบกลับมาว่าอย่างไร ส่วนการรับและตอบรับรายงานการนำส่ง ดูที่ [Webhook และรายงานการนำส่ง](/th/best-practices/webhooks) | การทำงาน | Route | | -------------- | ---------------------------------------------------------------------------- | | ประวัติการส่ง | `GET /organization/{organizationId}/webhook-deliveries` | | ส่งซ้ำ | `POST /organization/{organizationId}/webhook-deliveries/{deliveryId}/replay` | | ทดสอบ endpoint | `POST /webhook-test` | `{organizationId}` คือค่าเดียวกับที่ส่งใน `X-TenantID` ## ประวัติการส่ง ```bash curl "$ME_BASE/organization/$ME_TENANT/webhook-deliveries?status=failed&limit=50" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` | Query | กรองตาม | | ----------------- | ---------------------- | | `endpoint_id` | endpoint ที่ตั้งค่าไว้ | | `event_id` | event รายการเดียว | | `status` | ผลการส่ง | | `q` | ข้อความอิสระ | | `limit`, `offset` | การแบ่งหน้า | ## ส่งซ้ำ ส่งรายการที่ endpoint ของคุณพลาดไปอีกครั้ง เหมาะกับตอนที่เพิ่งแก้บั๊กที่ทำให้ตอบ 5xx ```bash curl -X POST "$ME_BASE/organization/$ME_TENANT/webhook-deliveries/$DELIVERY_ID/replay" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` ## ทดสอบ endpoint ยิงคำขอครั้งเดียวไปที่ URL เพื่อดูว่าตอบกลับอย่างไร ก่อนจะต่อ event จริงเข้าไป ```bash curl -X POST "$ME_BASE/webhook-test" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/me", "method": "POST", "headers": [{ "key": "X-Secret", "value": "s3cr3t" }], "body": { "ping": true } }' ``` ```json { "success": true, "status": 200, "statusText": "OK", "headers": { "content-type": "application/json" }, "data": { "ok": true } } ``` ::: warning `success` ไม่ได้แปลว่า `2xx` `success` บอกว่าคำขอ**ส่งสำเร็จ** ไม่ได้บอกว่า endpoint ของคุณรับไว้ ให้ตรวจค่า `status` ด้วย ::: Method ที่ใช้ได้: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD` ## Go SDK ```go history, err := c.Webhooks.ListDeliveries(ctx, &sdk.ListDeliveriesOptions{Status: "failed"}) _, err = c.Webhooks.Replay(ctx, deliveryID) res, err := c.Webhooks.Test(ctx, &sdk.WebhookTestRequest{URL: url, Method: "POST"}) ``` --- # World Data Source: /th/capabilities/world-data # World Data **World Data** คือข้อมูลอ้างอิง — ข้อมูลที่เหมือนกันสำหรับทุกผู้เรียกใช้ และเปลี่ยนแปลง ปีละไม่กี่ครั้ง ปัจจุบันคือรายการรหัสโทรศัพท์ของแต่ละประเทศ มีไว้เพื่อให้ช่องกรอกเบอร์โทรไม่ต้องเก็บรายการนี้เอง ตารางที่ hardcode ไว้จะเก่าโดยไม่มีใคร รู้ตัว และจะไม่มีใครสังเกตว่าประเทศไหนหายไปจนกว่าจะมีคนจากประเทศนั้นสมัครไม่ได้ | การกระทำ | Route | | ------------------ | -------------------------- | | ดูรายการรหัสประเทศ | `GET /worlddata/countries` | ::: info รับ publishable key ได้ด้วย ข้อมูลรหัสโทรศัพท์เหมือนกันสำหรับทุกผู้เรียกและไม่ให้สิทธิ์ใด ๆ จึงเป็นพื้นผิวเดียวที่รับ publishable key (`pk_…`) จาก Origin ที่ผูกไว้ได้ด้วย โดยใช้ header `API-Key` แทน ::: ## ดูรายการรหัสโทรศัพท์ของประเทศ ``` GET /worlddata/countries?page=1&limit=100 API-Key: $ME_API_KEY ``` ```json { "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 } ``` | ฟิลด์ | ความหมาย | | ----------- | ----------------------------------------- | | `code` | ISO-3166 alpha-2 ตัวพิมพ์ใหญ่ | | `dial_code` | รหัสโทร E.164 รวมเครื่องหมาย `+` ด้านหน้า | | `name` | ชื่อประเทศภาษาอังกฤษ | ## การแบ่งหน้า `page` เริ่มที่ 1 และทั้งสองค่าจะถูกส่งกลับมาด้วย ผู้เรียกใช้จึงเดินตาม `total_pages` ได้โดยไม่ต้องเก็บ cursor เอง | ค่าที่ส่งเข้ามา | ผลลัพธ์ | | ------------------------------ | ---------------------------------------- | | `limit` มากกว่า 250 | ถูกจำกัดไว้ที่ 250 | | `limit` หรือ `page` ไม่ถูกต้อง | กลับไปใช้ค่าเริ่มต้น (page 1, limit 100) | | `page` เกินหน้าสุดท้าย | `data` ว่าง แต่ยังได้ `200` | หน้าที่เกินรายการสุดท้ายจะคืน list ว่างแทนที่จะเป็น `404` เพื่อให้ผู้เรียกใช้ที่เดินจนสุด ไม่ต้องเขียนกรณีพิเศษสำหรับแถวถัดจากแถวสุดท้าย ::: warning ประเทศในกลุ่ม NANP ใช้ prefix เต็ม ประเทศในกลุ่ม North American Numbering Plan ใช้รหัสประเทศ `1` ร่วมกัน แต่มี prefix ต่างกัน — American Samoa คือ `+1684` และ Anguilla คือ `+1264` อย่าตัดให้เหลือ `+1` เพราะเบอร์ที่ได้จะโทรออกไม่ได้ ::: ## การ Cache ข้อมูลชุดนี้เปลี่ยนปีละไม่กี่ครั้ง จะ cache ไว้นานเท่าที่เหมาะสมแล้วรีเฟรชตอน deploy ก็ได้ ไม่จำเป็นต้องเรียกทุก request --- # Authentication Source: /th/overview/authentication # Authentication ทุก Request ที่ส่งไปยัง Platform API ต้องมี Header ที่จำเป็น 2 ตัวดังนี้ | Header | จำเป็น | วัตถุประสงค์ | | ------------ | ------ | -------------------------------------------- | | `API-Key` | ✅ | API key ของคุณ สร้างได้จาก developer console | | `X-TenantID` | ✅ | องค์กร (workspace) ที่ Request นั้นทำงานอยู่ | ```bash curl "$ME_BASE/voice/providers" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` ::: danger เก็บ Credentials ไว้ฝั่ง Server เท่านั้น `API-Key` คือข้อมูลลับ ห้ามฝังไว้ในเบราว์เซอร์ แอปมือถือ หรือ Client ใดก็ตามที่คุณไม่ได้ควบคุม ให้ Proxy Request ผ่าน Backend ของคุณเองเสมอ ::: ## ขอบเขตที่ key เข้าถึงได้ Key ถือสิทธิ์ของ**เจ้าของ key** ระบบจะยืนยันสิทธิ์ในฐานะผู้ใช้ที่เป็นเจ้าของ key เทียบกับองค์กรใน `X-TenantID` ดังนั้น key เข้าถึงได้เท่าที่เจ้าของเข้าถึงได้ ไม่มากกว่านั้น Key ไม่ได้จำกัดสิทธิ์ให้แคบลงกว่าผู้ใช้คนนั้น ให้สร้าง key จากบัญชีที่มีสิทธิ์ตรงกับ ที่ระบบเชื่อมต่อต้องใช้จริง และเปลี่ยน key ทันทีหากหลุด ## ข้อผิดพลาด | Status | ความหมาย | วิธีแก้ไข | | ------ | ----------------------------------------------- | --------------------------------------------------- | | `401` | `API-Key` หรือ `X-TenantID` หายไปหรือไม่ถูกต้อง | ตรวจสอบว่า Header ทั้งสองมีค่าและเป็นปัจจุบัน | | `400` | Request Body ผิดรูปแบบ | ตรวจสอบให้ตรงกับ [API Reference](/th/api-reference) | | `404` | ไม่พบ Resource ที่ระบุ | ตรวจสอบ Path และ `{uuid}` ให้ถูกต้อง | Response ของข้อผิดพลาดทุกประเภทมีโครงสร้างเดียวกัน: ```json { "code": "invalid_request", "message": "phone_number is required", "type": "validation" } ``` ## Base URLs | Environment | Base URL | | ----------- | ------------------------------------------------------- | | Production | `https://api.messageeagle.com/api/v1/app1/platform` | | Sandbox | `https://sandbox.messageeagle.com/api/v1/app1/platform` | ไปต่อที่ [SMS Messaging](/th/capabilities/sms) --- # บทนำ Source: /th/overview/introduction # บทนำ MessageEagle คือแพลตฟอร์มข้อความสำหรับ **SMS**, **Voice** และ **Verify (OTP)** ด้วยชุด Credentials ชุดเดียวและ Base URL เดียว คุณจะสามารถส่งข้อความแบบ Transactional และ Broadcast โทรหาลูกค้าด้วย Voice และ IVR รวมถึงส่งและยืนยันรหัสผ่านครั้งเดียว (OTP) ผ่านทุกค่ายมือถือชั้นนำ ::: tip รีบใช้งาน? ข้ามไปที่ [เริ่มต้นอย่างรวดเร็ว](/th/overview/quickstart) เพื่อส่งข้อความแรกของคุณ หรือเปิด [API Reference](/th/api-reference) แบบ Interactive เพื่อทดลองเรียก Endpoint ในเบราว์เซอร์ได้เลย ::: ## โครงสร้างของ API Endpoint สำหรับธุรกิจทั้งหมดอยู่ภายใต้ Base Path เดียว: ``` https://api.messageeagle.com/api/v1/app1/platform ``` Request และ Response เป็นรูปแบบ JSON ทุกการเรียก API ต้องส่ง Header 2 ตัวเพื่อยืนยันตัวตน — ดูรายละเอียดที่ [Authentication](/th/overview/authentication) | ฟีเจอร์ | ความสามารถ | เริ่มต้น | | ---------------- | ------------------------------------------------ | --------------------------------------------- | | **SMS** | ข้อความแบบ Transactional, Broadcast และ EagleEye | [SMS Messaging](/th/capabilities/sms) | | **Voice** | โทร OTP, Broadcast, ผ่านเจ้าหน้าที่, IVR | [Voice](/th/capabilities/voice) | | **Verify** | ส่งและตรวจสอบรหัสผ่านครั้งเดียว | [Verify / OTP](/th/capabilities/verify) | | **Sender Names** | ลงทะเบียนและอนุมัติ Sender ID | [Sender Names](/th/capabilities/sender-names) | ## หลักการออกแบบ - **ผลลัพธ์รายบุคคล** Endpoint สำหรับการส่งจำนวนมากจะคืนสถานะสำหรับ **ทุก** ผู้รับ หมายเลขที่ผิดพลาดรายเดียวไม่ทำให้ทั้ง Batch ล้มเหลว — ตรวจสอบ `send_messages[].report` แยกตามรายการ - **Idempotency ผ่าน `transaction_uuid`** แนบ UUIDv7 กับแต่ละข้อความเพื่อเชื่อมโยงกับ Delivery-Report Webhook และการติดตามภายหลัง - **Templating ในตัว** เนื้อหาข้อความรองรับ Placeholder รูปแบบ {{first_name}} ที่ถูกแทนที่ด้วยข้อมูลเฉพาะผู้รับและ `meta_data` ## สิ่งที่คุณต้องเตรียม 1. **API key** (`API-Key`) และ **รหัสองค์กร** (`X-TenantID`) 2. **ชื่อผู้ส่งที่ได้รับอนุมัติ** อย่างน้อยหนึ่งชื่อ — ดูที่ [Sender Names](/th/capabilities/sender-names) 3. หมายเลขผู้รับในรูปแบบ **E.164** เช่น `+66886578553` พร้อมแล้ว? ไปต่อที่ [เริ่มต้นอย่างรวดเร็ว](/th/overview/quickstart) --- # เริ่มต้นอย่างรวดเร็ว Source: /th/overview/quickstart # เริ่มต้นอย่างรวดเร็ว คู่มือนี้จะนำคุณส่ง SMS แบบ Transactional หนึ่งรายการ คุณต้องมี `API-Key`, `X-TenantID` และ [ชื่อผู้ส่งที่ได้รับอนุมัติ](/th/capabilities/sender-names) อย่างน้อยหนึ่งชื่อ ## 1. ตั้งค่า Credentials ```bash export ME_API_KEY="your-app-token" export ME_TENANT="your-tenant-id" export ME_BASE="https://api.messageeagle.com/api/v1/app1/platform" ``` ## 2. ส่งข้อความ ::: code-group ```bash [cURL] curl -X POST "$ME_BASE/sms/send_messages/message" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "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" } ] }' ``` ```go [Go SDK] package main import ( "context" "log" "github.com/lodashventure/messageeagle/sms-voice/sdk" ) func main() { client, err := sdk.New(sdk.Config{ BaseURL: "https://api.messageeagle.com", APIKey: "your-api-key", TenantID: "your-organization-id", }) if err != nil { log.Fatal(err) } resp, err := client.SMS.SendMessage(context.Background(), &sdk.SendMessageRequest{ CheckSpamMsgMode: "not_check", Data: []sdk.SendMessageItem{{ SenderName: "MyBrand", Operator: "ais", PhoneNumber: "+66886578553", Message: "Hi Mali, your order has shipped.", }}, }) if err != nil { log.Fatal(err) } log.Printf("accepted=%d rejected=%d batches=%+v", resp.Accepted, resp.Rejected, resp.Batches) } ``` ```ts [Fetch] const res = await fetch(`${ME_BASE}/sms/send_messages/message`, { method: "POST", headers: { "API-Key": ME_API_KEY, "X-TenantID": ME_TENANT, "Content-Type": "application/json", }, body: JSON.stringify({ 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", }, ], }), }); console.log(await res.json()); ``` ::: ## 3. อ่านผลลัพธ์ ผลลัพธ์รายงานการกระจายข้อความ โดยห่อไว้ใน `data` แถวที่ใช้ operator + ผู้ส่ง + ข้อความเดียวกันจะถูกรวมเป็นหนึ่ง batch ```json { "data": { "accepted": 1, "rejected": 0, "batches": [ { "batch_id": "018f7b2a-7c3e-7a1b-9c2d-1a2b3c4d5e6f", "accepted": 1, "rejected": 0 } ] } } ``` ::: tip `undeliverable_chars` batch อาจมี `undeliverable_chars` ซึ่งคือตัวอักษรที่ผู้ให้บริการทำหายระหว่างทาง ความเสียหายนี้มองไม่เห็นจากภายนอก เพราะข้อความถูกรับไว้ คิดค่าบริการแล้ว แต่เครื่องปลายทางไม่แสดงตัวอักษรนั้น ::: ::: warning ชื่อผู้ส่งต้องได้รับอนุมัติ ถ้า `sender_name` ยังไม่ได้รับอนุมัติสำหรับผู้ให้บริการปลายทาง ผู้รับเหล่านั้นจะถูกนับใน `rejected` แทนที่จะทำให้ทั้งคำขอล้มเหลว ::: ## ขั้นตอนถัดไป - [Authentication](/th/overview/authentication) — รายละเอียด Header และข้อผิดพลาด - [SMS Messaging](/th/capabilities/sms) — การส่งแบบ Broadcast, การจัดกำหนดเวลา และ Templating - [Webhooks & Delivery Reports](/th/best-practices/webhooks) — รับการแจ้งเตือนเมื่อข้อความถูกส่งถึงผู้รับ --- # สารบัญ Source: /th/table-of-content # สารบัญ รวมทุกหน้าในเอกสาร MessageEagle ไว้ในหน้าเดียว ใช้เป็นแผนที่นำทาง หรือส่งทั้งไซต์ให้ LLM ผ่านตัวเลือก **LLMs full** ในเมนู Copy page ด้านบน ## เริ่มต้นใช้งาน - [บทนำ](/th/overview/introduction) — MessageEagle คืออะไร และโครงสร้างของ API เป็นอย่างไร - [เริ่มต้นอย่างรวดเร็ว](/th/overview/quickstart) — ส่งข้อความแรกของคุณในไม่ถึงห้านาที - [การยืนยันตัวตน](/th/overview/authentication) — header `API-Key` และ `X-TenantID` ## ความสามารถ - [SMS Messaging](/th/capabilities/sms) — การส่งแบบ transactional, broadcast และ EagleEye - [Voice](/th/capabilities/voice) — OTP, broadcast, การโทรผ่าน agent, session และ IVR - [โทรออก](/th/capabilities/place-calls) — ให้เบอร์ดัง อ่านข้อความ ต่อ agent หรือทำแบบสำรวจ - [ควบคุมสายที่กำลังสนทนา](/th/capabilities/calls) — พักสาย โหมดหัวหน้างาน วางสาย และผู้เข้าร่วมในสายที่กำลังคุย - [Voice Agent](/th/capabilities/agents) — การตั้งค่า agent และรายการผู้ให้บริการเสียง - [Verify / OTP](/th/capabilities/verify) — ส่งและยืนยันรหัส one-time password - [Sender Names](/th/capabilities/sender-names) — ลงทะเบียนและอนุมัติ sender ID - [World Data](/th/capabilities/world-data) — รหัสโทรศัพท์ของแต่ละประเทศและข้อมูลอ้างอิงอื่น ๆ - [การส่ง Webhook](/th/capabilities/webhook-deliveries) — ประวัติการส่ง ส่งซ้ำ และทดสอบ endpoint ## แนวปฏิบัติที่ดี - [แนวปฏิบัติสำหรับการส่งข้อความ](/th/best-practices/best-practices) — เขียน กำหนดเส้นทาง และสร้างเทมเพลตข้อความให้ส่งถึงผู้รับ - [Rate Limits & Quotas](/th/best-practices/rate-limits) — ระบบ throttling ทำงานอย่างไร และวิธีอยู่ภายใต้ขีดจำกัด - [Webhooks & รายงานการส่ง](/th/best-practices/webhooks) — รับ status callback อย่างน่าเชื่อถือ ## อ้างอิง - [API Reference](/th/api-reference) — เอกสาร reference แบบโต้ตอบ สามารถทดลองใช้งานได้ทุก endpoint - [OpenAPI document](/openapi.yaml) — spec OpenAPI 3.1 แบบดิบ - [llms.txt](/llms.txt) · [llms-full.txt](/llms-full.txt) — ดัชนีที่ LLM อ่านได้ --- # zh-Hans/index Source: /zh-Hans --- # 语音智能体 Source: /zh-Hans/capabilities/agents # 语音智能体 这里有两样东西:决定智能体行为的**配置**,以及智能体说话可用的**语音与供应商清单**。 | 操作 | Route | | ------------ | ---------------------------------------- | | 列出配置 | `GET /agents/configs` | | 读取单个配置 | `GET /agents/configs/{documentId}` | | 创建 | `POST /agents/configs` | | 替换 | `PUT /agents/configs/{documentId}` | | 删除 | `DELETE /agents/configs/{documentId}` | | 列出供应商 | `GET /voice/providers` | | 列出语音 | `GET /voice/providers/{provider}/voices` | ## 配置 配置是一份文档。随着智能体能力增加,studio 会往里加字段,因此遇到不认识的键请原样透传,不要丢弃。 ## 供应商与语音清单 可用的供应商**因部署而异**,不是固定值,取决于该环境配置了哪些 vendor key。请在运行时读取清单,不要把供应商名写死在代码里。 ```json { "data": [ { "id": "voice_abc", "name": "Mali", "gender": "female", "language": "th-TH", "preview": true } ] } ``` ::: tip 请遵循 `preview` `preview` 会随 vendor key 变化,因此每次请求都会重新判定。为 `false` 时请禁用播放按钮,不要改用其他语音代替——否则用户听到的并不是智能体真正会用的声音。 ::: --- # 通话控制 Source: /zh-Hans/capabilities/calls # 通话控制 这组端点作用于**已经在进行中**的通话,使用 LiveKit 房间名而不是 session id 来定位。因为在双方还在通话时,真正存在的是房间。 | 操作 | Route | 用途 | | ------------ | -------------------------------------------------------- | -------------------------- | | **保持** | `POST /voice/calls/{roomName}/hold` | 让对方进入保持,或恢复通话 | | **模式** | `POST /voice/calls/{roomName}/mode` | 切换主管的监听模式 | | **挂断** | `POST /voice/calls/{roomName}/hangup` | 结束通话并封存 session | | **锁定** | `POST /voice/calls/{roomName}/lock` | 阻止其他人再加入 | | **移出成员** | `DELETE /voice/calls/{roomName}/participants/{identity}` | 移出一位参与者 | | **强制关闭** | `POST /voice/calls/{uuid}/force-close` | 关闭房间已不存在的 session | ::: danger API 密钥可以操作进行中的通话 在这组路由上,密钥携带的是**所有者的权限**。拿到密钥的人可以挂断甚至接管正在进行的通话。请只保存在服务端,一旦泄露立即重新签发。 ::: ## 保持与恢复 保持会做两件事:将双方静音,并播放等待音。即使没有配置等待音,静音也依然生效,因此被保持的一方不会听到坐席这端的声音。 ```bash curl -X POST "$ME_BASE/voice/calls/$ROOM/hold" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{"muted": true, "holder_identity": "agent-42"}' ``` ```json { "status": "ok", "held": true } ``` 向同一 route 发送 `"muted": false` 即可恢复通话。 --- # Webhook 投递 Source: /zh-Hans/capabilities/webhook-deliveries # Webhook 投递 本页是 webhook 的**发出侧**:MessageEagle 尝试发送了什么,以及你的端点如何响应。关于接收投递回执,请参阅英文文档 [Webhooks & Delivery Reports](/en/best-practices/webhooks)。 | 操作 | Route | | -------- | ---------------------------------------------------------------------------- | | 投递历史 | `GET /organization/{organizationId}/webhook-deliveries` | | 重新投递 | `POST /organization/{organizationId}/webhook-deliveries/{deliveryId}/replay` | | 测试端点 | `POST /webhook-test` | `{organizationId}` 与你在 `X-TenantID` 中发送的值相同。 ## 测试端点 在接入真实事件之前,先向 URL 发一次请求,看看它如何响应。 ```bash curl -X POST "$ME_BASE/webhook-test" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/me", "method": "POST", "headers": [{ "key": "X-Secret", "value": "s3cr3t" }], "body": { "ping": true } }' ``` ::: warning `success` 不等于 `2xx` `success` 只表示请求**已完成**,并不表示对方接受了它。请同时检查 `status`。 ::: --- # 认证 Source: /zh-Hans/overview/authentication # 认证 发往 Platform API 的每个请求都需要两个请求头。 | 请求头 | 必填 | 用途 | | ------------ | ---- | ----------------------------- | | `API-Key` | ✅ | 在开发者控制台签发的 API 密钥 | | `X-TenantID` | ✅ | 请求所属的组织(工作区)ID | ```bash curl "$ME_BASE/voice/providers" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` ::: danger 凭据只应保存在服务端 `API-Key` 是机密信息。不要嵌入浏览器、移动应用或任何你无法掌控的客户端,请通过自有后端转发请求。 ::: ## 密钥能访问的范围 密钥携带的是**其所有者的权限**。请求会以密钥所有者的身份、针对 `X-TenantID` 指定的组织进行授权。也就是说,密钥能访问的范围与所有者相同,不会更多。 密钥本身并不会收窄权限。请使用权限恰好够用的账号签发密钥;一旦泄露,立即重新签发。 ## 错误 | 状态码 | 含义 | 处理 | | ------ | ------------------------------------ | --------------------------------------- | | `401` | `API-Key` 或 `X-TenantID` 缺失或无效 | 确认两个请求头都存在且有效 | | `400` | 请求体格式有误 | 对照 [API 参考](/en/api-reference) 校验 | | `404` | 找不到目标 | 检查路径与 ID | --- # 简介 Source: /zh-Hans/overview/introduction # 简介 MessageEagle 是面向 **短信**、**语音** 和 **身份验证(OTP)** 的消息平台。一套凭据、一个基础 URL,即可完成消息发送、通话控制与语音智能体配置。 ::: tip 想直接上手? 从[快速开始](/zh-Hans/overview/quickstart)发出第一条消息,或在浏览器中打开 [API 参考](/en/api-reference)。 ::: ## API 结构 所有业务端点都位于同一基础路径之下。请求与响应均为 JSON,认证由两个请求头完成。 ``` https://api.messageeagle.com/api/v1/app1/platform ``` | 能力 | 作用 | 从这里开始 | | -------------- | ---------------------------- | -------------------------------------------------------- | | **短信** | 事务性发送与批量群发 | [快速开始](/zh-Hans/overview/quickstart) | | **通话控制** | 进行中通话的保持、模式与挂断 | [通话控制](/zh-Hans/capabilities/calls) | | **语音智能体** | 智能体配置与语音清单 | [语音智能体](/zh-Hans/capabilities/agents) | | **Webhook** | 投递历史、重投与测试 | [Webhook 投递](/zh-Hans/capabilities/webhook-deliveries) | ## 你需要准备 1. 一个 **API 密钥**(`API-Key`)和**组织 ID**(`X-TenantID`) 2. 至少一个已审核通过的**发送方名称** 3. **E.164** 格式的收件号码,例如 `+66886578553` ::: info i18n 本语言目前只翻译了部分页面。未翻译的页面请查看英文文档。 ::: --- # 快速开始 Source: /zh-Hans/overview/quickstart # 快速开始 本指南发送一条短信。你需要 `API-Key`、`X-TenantID`,以及一个已审核通过的发送方名称。 ## 1. 设置凭据 ```bash export ME_API_KEY="your-api-key" export ME_TENANT="your-organization-id" export ME_BASE="https://api.messageeagle.com/api/v1/app1/platform" ``` ## 2. 发送消息 ```bash curl -X POST "$ME_BASE/sms/send_messages/message" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "check_spam_msg_mode": "not_check", "data": [ { "sender_name": "MyBrand", "operator": "ais", "message": "Hello", "phone_number": "+66886578553" } ] }' ``` ## 3. 读取结果 响应包裹在 `data` 中返回。operator、发送方名称与正文相同的行会合并为同一个 batch。 ```json { "data": { "accepted": 1, "rejected": 0, "batches": [ { "batch_id": "018f7b2a-7c3e-7a1b-9c2d-1a2b3c4d5e6f", "accepted": 1, "rejected": 0 } ] } } ``` ::: warning 发送方名称必须已审核 若 `sender_name` 在目标运营商处尚未审核通过,这些收件人会计入 `rejected`,而不会导致整个请求失败。 ::: ## 后续步骤 - [认证](/zh-Hans/overview/authentication) - [通话控制](/zh-Hans/capabilities/calls) - [Webhook 投递](/zh-Hans/capabilities/webhook-deliveries) --- # 目录 Source: /zh-Hans/table-of-content # 目录 ## 开始使用 - [简介](/zh-Hans/overview/introduction) — MessageEagle 概览与 API 结构 - [快速开始](/zh-Hans/overview/quickstart) — 五分钟内发出第一条消息 - [认证](/zh-Hans/overview/authentication) — `API-Key` 与 `X-TenantID` 请求头 ## 能力 - [通话控制](/zh-Hans/capabilities/calls) — 进行中通话的保持、模式、挂断与成员管理 - [语音智能体](/zh-Hans/capabilities/agents) — 智能体配置与语音清单 - [Webhook 投递](/zh-Hans/capabilities/webhook-deliveries) — 投递历史、重投与端点测试 ## 参考 - [API 参考](/en/api-reference) — 可在浏览器中直接试用的交互式参考(英文) ::: info i18n 本语言目前只翻译了部分页面。未翻译的页面请查看英文文档。 ::: --- # zh-Hant/index Source: /zh-Hant --- # 語音代理 Source: /zh-Hant/capabilities/agents # 語音代理 這裡有兩樣東西:決定代理行為的**設定**,以及代理說話時可用的**語音與供應商清單**。 | 操作 | Route | | ------------ | ---------------------------------------- | | 列出設定 | `GET /agents/configs` | | 讀取單一設定 | `GET /agents/configs/{documentId}` | | 建立 | `POST /agents/configs` | | 取代 | `PUT /agents/configs/{documentId}` | | 刪除 | `DELETE /agents/configs/{documentId}` | | 列出供應商 | `GET /voice/providers` | | 列出語音 | `GET /voice/providers/{provider}/voices` | ## 設定 設定是一份文件。隨著代理能力增加,studio 會往裡面加欄位,因此遇到不認識的鍵請原樣傳遞,不要丟棄。 ## 供應商與語音清單 可用的供應商**因部署而異**,並非固定值,取決於該環境設定了哪些 vendor key。請在執行時讀取清單,不要把供應商名稱寫死在程式碼裡。 ```json { "data": [ { "id": "voice_abc", "name": "Mali", "gender": "female", "language": "th-TH", "preview": true } ] } ``` ::: tip 請遵循 `preview` `preview` 會隨 vendor key 變動,因此每次請求都會重新判定。為 `false` 時請停用播放按鈕,不要改用其他語音替代——否則使用者聽到的並不是代理真正會用的聲音。 ::: --- # 通話控制 Source: /zh-Hant/capabilities/calls # 通話控制 這組端點作用於**已經在進行中**的通話,以 LiveKit 房間名稱而非 session id 定位。因為在雙方還在通話時,真正存在的是房間。 | 操作 | Route | 用途 | | ------------ | -------------------------------------------------------- | -------------------------- | | **保留** | `POST /voice/calls/{roomName}/hold` | 讓對方進入保留,或恢復通話 | | **模式** | `POST /voice/calls/{roomName}/mode` | 切換主管的監聽模式 | | **掛斷** | `POST /voice/calls/{roomName}/hangup` | 結束通話並封存 session | | **鎖定** | `POST /voice/calls/{roomName}/lock` | 阻止其他人再加入 | | **移除成員** | `DELETE /voice/calls/{roomName}/participants/{identity}` | 移除一位參與者 | | **強制關閉** | `POST /voice/calls/{uuid}/force-close` | 關閉房間已不存在的 session | ::: danger API 金鑰可以操作進行中的通話 在這組路由上,金鑰帶的是**擁有者的權限**。取得金鑰的人可以掛斷甚至接手正在進行的通話。請只保存在伺服器端,一旦外洩立即重新簽發。 ::: ## 保留與恢復 保留會做兩件事:將雙方靜音,並播放等待音樂。即使沒有設定等待音樂,靜音仍然生效,因此被保留的一方不會聽到客服這端的聲音。 ```bash curl -X POST "$ME_BASE/voice/calls/$ROOM/hold" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{"muted": true, "holder_identity": "agent-42"}' ``` ```json { "status": "ok", "held": true } ``` 向同一個 route 傳送 `"muted": false` 即可恢復通話。 --- # Webhook 傳送 Source: /zh-Hant/capabilities/webhook-deliveries # Webhook 傳送 本頁是 webhook 的**送出側**:MessageEagle 嘗試傳送了什麼,以及你的端點如何回應。關於接收傳送回條,請參閱英文文件 [Webhooks & Delivery Reports](/en/best-practices/webhooks)。 | 操作 | Route | | -------- | ---------------------------------------------------------------------------- | | 傳送紀錄 | `GET /organization/{organizationId}/webhook-deliveries` | | 重新傳送 | `POST /organization/{organizationId}/webhook-deliveries/{deliveryId}/replay` | | 測試端點 | `POST /webhook-test` | `{organizationId}` 與你在 `X-TenantID` 中傳送的值相同。 ## 測試端點 在接上真實事件之前,先向 URL 發一次請求,看看它如何回應。 ```bash curl -X POST "$ME_BASE/webhook-test" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/me", "method": "POST", "headers": [{ "key": "X-Secret", "value": "s3cr3t" }], "body": { "ping": true } }' ``` ::: warning `success` 不等於 `2xx` `success` 只代表請求**已完成**,並不代表對方接受了它。請一併檢查 `status`。 ::: --- # 驗證 Source: /zh-Hant/overview/authentication # 驗證 送往 Platform API 的每個請求都需要兩個標頭。 | 標頭 | 必填 | 用途 | |---|---|---| | `API-Key` | ✅ | 在開發者主控台簽發的 API 金鑰 | | `X-TenantID` | ✅ | 請求所屬的組織(工作區)ID | ```bash curl "$ME_BASE/voice/providers" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" ``` ::: danger 憑證只應保存在伺服器端 `API-Key` 是機密資訊。請勿嵌入瀏覽器、行動應用或任何你無法掌控的用戶端,請透過自有後端轉送請求。 ::: ## 金鑰可觸及的範圍 金鑰帶的是**其擁有者的權限**。請求會以金鑰擁有者的身分、針對 `X-TenantID` 指定的組織進行授權。也就是說,金鑰能觸及的範圍與擁有者相同,不會更多。 金鑰本身不會縮小權限。請用權限剛好足夠的帳號簽發金鑰;一旦外洩,立即重新簽發。 ## 錯誤 | 狀態碼 | 意義 | 處理 | |---|---|---| | `401` | `API-Key` 或 `X-TenantID` 缺少或無效 | 確認兩個標頭都存在且有效 | | `400` | 請求內容格式有誤 | 對照 [API 參考](/en/api-reference) 檢查 | | `404` | 找不到目標 | 檢查路徑與 ID | --- # 簡介 Source: /zh-Hant/overview/introduction # 簡介 MessageEagle 是面向 **簡訊**、**語音** 與 **身分驗證(OTP)** 的訊息平台。一組憑證、一個基礎 URL,即可完成訊息傳送、通話控制與語音代理設定。 ::: tip 想直接上手? 從[快速開始](/zh-Hant/overview/quickstart)送出第一則訊息,或在瀏覽器中開啟 [API 參考](/en/api-reference)。 ::: ## API 結構 所有業務端點都位於同一個基礎路徑之下。請求與回應皆為 JSON,驗證由兩個標頭完成。 ``` https://api.messageeagle.com/api/v1/app1/platform ``` | 能力 | 作用 | 從這裡開始 | |---|---|---| | **簡訊** | 交易型傳送與大量群發 | [快速開始](/zh-Hant/overview/quickstart) | | **通話控制** | 進行中通話的保留、模式與掛斷 | [通話控制](/zh-Hant/capabilities/calls) | | **語音代理** | 代理設定與語音清單 | [語音代理](/zh-Hant/capabilities/agents) | | **Webhook** | 傳送紀錄、重送與測試 | [Webhook 傳送](/zh-Hant/capabilities/webhook-deliveries) | ## 你需要準備 1. 一組 **API 金鑰**(`API-Key`)與**組織 ID**(`X-TenantID`) 2. 至少一個已審核通過的**寄件者名稱** 3. **E.164** 格式的收件號碼,例如 `+66886578553` ::: info i18n 本語言目前僅翻譯部分頁面。未翻譯的頁面請參閱英文文件。 ::: --- # 快速開始 Source: /zh-Hant/overview/quickstart # 快速開始 本指南會傳送一則簡訊。你需要 `API-Key`、`X-TenantID`,以及一個已審核通過的寄件者名稱。 ## 1. 設定憑證 ```bash export ME_API_KEY="your-api-key" export ME_TENANT="your-organization-id" export ME_BASE="https://api.messageeagle.com/api/v1/app1/platform" ``` ## 2. 傳送訊息 ```bash curl -X POST "$ME_BASE/sms/send_messages/message" \ -H "API-Key: $ME_API_KEY" \ -H "X-TenantID: $ME_TENANT" \ -H "Content-Type: application/json" \ -d '{ "check_spam_msg_mode": "not_check", "data": [ { "sender_name": "MyBrand", "operator": "ais", "message": "Hello", "phone_number": "+66886578553" } ] }' ``` ## 3. 讀取結果 回應會包在 `data` 中。operator、寄件者名稱與內文相同的列會合併為同一個 batch。 ```json { "data": { "accepted": 1, "rejected": 0, "batches": [ { "batch_id": "018f7b2a-7c3e-7a1b-9c2d-1a2b3c4d5e6f", "accepted": 1, "rejected": 0 } ] } } ``` ::: warning 寄件者名稱必須先通過審核 若 `sender_name` 在目標電信業者尚未通過審核,這些收件人會計入 `rejected`,不會讓整個請求失敗。 ::: ## 後續步驟 - [驗證](/zh-Hant/overview/authentication) - [通話控制](/zh-Hant/capabilities/calls) - [Webhook 傳送](/zh-Hant/capabilities/webhook-deliveries) --- # 目錄 Source: /zh-Hant/table-of-content # 目錄 ## 開始使用 - [簡介](/zh-Hant/overview/introduction) — MessageEagle 概覽與 API 結構 - [快速開始](/zh-Hant/overview/quickstart) — 五分鐘內送出第一則訊息 - [驗證](/zh-Hant/overview/authentication) — `API-Key` 與 `X-TenantID` 標頭 ## 能力 - [通話控制](/zh-Hant/capabilities/calls) — 進行中通話的保留、模式、掛斷與成員管理 - [語音代理](/zh-Hant/capabilities/agents) — 代理設定與語音清單 - [Webhook 傳送](/zh-Hant/capabilities/webhook-deliveries) — 傳送紀錄、重送與端點測試 ## 參考 - [API 參考](/en/api-reference) — 可在瀏覽器中直接試用的互動式參考(英文) ::: info i18n 本語言目前僅翻譯部分頁面。未翻譯的頁面請參閱英文文件。 :::