API reference · v2
API documentation
The FasterMessage REST contract: send SMS, WhatsApp messages, emails and voice calls, follow every delivery in real time and steer your balance from a single interface.
Quick start
The FasterMessage API sends messages over four channels — SMS, WhatsApp, email and voice — from a single REST contract. Three steps stand between a new account and a first delivered message.
1. Collect your credentials
In the back office, open Integration & API. There you find your account identifier, and there you generate two keys: a test key and a live key. A key is shown only once — it is stored hashed and cannot be read back.
2. Declare a sender
SMS requires an approved Sender ID: there is no per-account default sender, and a send without from is refused before it is even priced. Declaration happens in the back office, Sender IDs tab.
3. Send
curl -X POST https://api-next.fastermessage.com/v2/sms/send \ -u "$FM_API_USER:$FM_API_SECRET" \ -H "X-Api-Key: $FM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "+22960000000", "from": "FASTERMSG", "content": "Your code is 480912. Valid for 5 min." }'
const auth = Buffer.from(`${process.env.FM_API_USER}:${process.env.FM_API_SECRET}`).toString('base64'); const response = await fetch('https://api-next.fastermessage.com/v2/sms/send', { method: 'POST', headers: { Authorization: `Basic ${auth}`, 'X-Api-Key': process.env.FM_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ "to": "+22960000000", "from": "FASTERMSG", "content": "Your code is 480912. Valid for 5 min." }), }); const data = await response.json(); console.log(response.status, data);
<?php $ch = curl_init('https://api-next.fastermessage.com/v2/sms/send'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_USERPWD => getenv('FM_API_USER') . ':' . getenv('FM_API_SECRET'), CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('FM_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'to' => '+22960000000', 'from' => 'FASTERMSG', 'content' => 'Your code is 480912. Valid for 5 min.', ]), ]); $response = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $status . PHP_EOL . $response . PHP_EOL;
import os import requests response = requests.post( "https://api-next.fastermessage.com/v2/sms/send", auth=(os.environ["FM_API_USER"], os.environ["FM_API_SECRET"]), headers={"X-Api-Key": os.environ["FM_API_KEY"]}, json={ "to": "+22960000000", "from": "FASTERMSG", "content": "Your code is 480912. Valid for 5 min.", }, timeout=15, ) print(response.status_code, response.json())
{
"id": "msg_2f1c9d4e8a6b",
"channel": "sms",
"status": "queued",
"to": "22960000000",
"from": "FASTERMSG",
"units": 1,
"price": null,
"currency": "XOF",
"refund": null,
"client_reference": null,
"fallback_of": null,
"error": null,
"created_at": "2026-09-07T10:00:00.000Z",
"scheduled_at": null,
"sent_at": null,
"delivered_at": null
}Authentication
Every call carries two credentials, both mandatory: a Basic authentication and an API key. This is not redundancy — the two do not designate the same thing.
| Credential | Sent through | Designates |
|---|---|---|
| Basic | Authorization | The account to charge: it is the one paying for the send. |
| API key | X-Api-Key | The account making the request — itself, or one of its sub-accounts. |
The match between the two is checked on every call: a key belonging neither to the authenticated account nor to one of its sub-accounts is refused. That pairing is what lets a reseller send on behalf of its customers without handing them its password.
Authorization: Basic bW9uLWNvbXB0ZTpzZWNyZXQ= X-Api-Key: fm_live_xxxxxxxxxxxxxxxxxxxxxxxx Content-Type: application/json
Keep both server-side. A key exposed in a web or mobile client is a published key: revoke it from the back office and generate a new one.
Environments
| Environment | Key prefix | Behaviour |
|---|---|---|
| test | fm_test_… | Calls go through exactly the same checks as in production and return the same error codes, but nothing is handed to the carrier, nothing is charged, and no callback URL is called. |
| production | fm_live_… | Real sends, charged to the balance, handed to the carrier, genuine DLRs. |
What changes in test
- No carrier delivery: the recipient receives nothing, however valid their number.
- No charge — and a balance is not even required: you can write your whole integration before buying anything.
- Your brand’s Sender IDs can be borrowed even without an approval of their own — which is never the case in production.
- An immediate send is marked
deliveredat once; a scheduled send stayspendinguntil its date. No webhook is called: reaching your callback URL would already be a delivery. - The two environments are sealed off from each other: a test key reads back only its own simulated sends (
GET /v2/messages), never your real traffic — and the other way round. Simulations are kept for 30 days, then erased. - Code verification (OTP): the code is always a run of zeros (
000000, or0000depending on the length requested). Since no SMS goes out, this is what lets you exercisePOST /v2/otp/verifyend to end.
Try the API
Compose a call with your test credentials and run it from this page. The equivalent snippet is regenerated live in all four languages.
Test environment only — never use your production credentials on this page. The values you type stay in your browser: they are neither stored nor sent to a third party.
curl -X POST https://api-next.fastermessage.com/v2/sms/send \ -u "$FM_API_USER:$FM_API_SECRET" \ -H "X-Api-Key: $FM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "", "from": "FASTERMSG", "content": "Your code is 480912. Valid for 5 min." }'
Fill in the required fields to run the call.
Single send
Sends an SMS to one recipient. Returns 202 Accepted and the message resource: delivery itself is asynchronous and is followed by webhook or through GET /v2/messages/{id}.
Request body
| Field | Type | Description |
|---|---|---|
| torequired | string | Recipient in E.164 format (8 to 15 digits, + optional). |
| fromrequired | string | Sender ID approved on the account. No default sender is applied; an account in test mode may use its brand’s Sender ID. |
| contentrequired | string | Message text. Beyond 160 GSM characters (70 in UCS-2), it is split into segments, each billed individually. |
| scheduled_atoptional | string | Send date in ISO 8601 UTC. An unreadable date is refused, never ignored. |
| callback_urloptional | string | Callback URL specific to this message — takes precedence over the account URL. |
| client_referenceoptional | string | Free-form reference, echoed as-is in the response and in webhooks. |
| fallbackoptional | array | Ordered cascade of fallback channels, each step tried only if the previous one did not succeed. |
Optional headers
| Field | Type | Description |
|---|---|---|
| Idempotency-Keyoptional | string | A unique key of your choosing. A replay with the same key returns the memorised response instead of resending the message. Kept for 24 h. |
curl -X POST https://api-next.fastermessage.com/v2/sms/send \ -u "$FM_API_USER:$FM_API_SECRET" \ -H "X-Api-Key: $FM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "+22960000000", "from": "FASTERMSG", "content": "Your order cmd-42 is ready.", "client_reference": "cmd-42", "callback_url": "https://example.test/webhooks/fastermessage" }'
const auth = Buffer.from(`${process.env.FM_API_USER}:${process.env.FM_API_SECRET}`).toString('base64'); const response = await fetch('https://api-next.fastermessage.com/v2/sms/send', { method: 'POST', headers: { Authorization: `Basic ${auth}`, 'X-Api-Key': process.env.FM_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ "to": "+22960000000", "from": "FASTERMSG", "content": "Your order cmd-42 is ready.", "client_reference": "cmd-42", "callback_url": "https://example.test/webhooks/fastermessage" }), }); const data = await response.json(); console.log(response.status, data);
<?php $ch = curl_init('https://api-next.fastermessage.com/v2/sms/send'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_USERPWD => getenv('FM_API_USER') . ':' . getenv('FM_API_SECRET'), CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('FM_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'to' => '+22960000000', 'from' => 'FASTERMSG', 'content' => 'Your order cmd-42 is ready.', 'client_reference' => 'cmd-42', 'callback_url' => 'https://example.test/webhooks/fastermessage', ]), ]); $response = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $status . PHP_EOL . $response . PHP_EOL;
import os import requests response = requests.post( "https://api-next.fastermessage.com/v2/sms/send", auth=(os.environ["FM_API_USER"], os.environ["FM_API_SECRET"]), headers={"X-Api-Key": os.environ["FM_API_KEY"]}, json={ "to": "+22960000000", "from": "FASTERMSG", "content": "Your order cmd-42 is ready.", "client_reference": "cmd-42", "callback_url": "https://example.test/webhooks/fastermessage", }, timeout=15, ) print(response.status_code, response.json())
{
"id": "msg_2f1c9d4e8a6b",
"channel": "sms",
"status": "queued",
"to": "22960000000",
"from": "FASTERMSG",
"units": 1,
"price": null,
"currency": "XOF",
"refund": null,
"client_reference": "cmd-42",
"created_at": "2026-09-07T10:00:00.000Z"
}{
"error": {
"type": "business_error",
"code": "insufficient_balance",
"message": "Insufficient balance to process the request.",
"request_id": "2f1c9d4e-8a6b-4c1e-9f2a-7d3b5e0c1a84"
}
}Bulk sending
Up to 500 SMS per call, in a single request — and a single unit as far as rate limits are concerned. Values set at batch level (from, content, scheduled_at, callback_url) are inherited by every line that does not carry its own: the same entry point serves broadcasting (one text, N numbers) and grouped transactional sending (one text per line).
Request body
| Field | Type | Description |
|---|---|---|
| messagesrequired | array | Lines of the batch, 500 at most. Each line accepts the fields of a single send. |
| fromoptional | string | Batch sender, inherited by lines that declare none. Mandatory — on the batch or on every line. |
| contentoptional | string | Batch text, inherited the same way. |
| scheduled_atoptional | string | Common send date, in ISO 8601 UTC. |
| callback_urloptional | string | Callback URL shared by every line. |
curl -X POST https://api-next.fastermessage.com/v2/sms/batch \ -u "$FM_API_USER:$FM_API_SECRET" \ -H "X-Api-Key: $FM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "FASTERMSG", "content": "Your delivery arrives tomorrow between 9 am and 12 pm.", "messages": [ { "to": "+22960000000", "client_reference": "cmd-42" }, { "to": "+22961000001", "client_reference": "cmd-43" }, { "to": "+22962000002", "content": "Your delivery arrives today.", "client_reference": "cmd-44" } ] }'
const auth = Buffer.from(`${process.env.FM_API_USER}:${process.env.FM_API_SECRET}`).toString('base64'); const response = await fetch('https://api-next.fastermessage.com/v2/sms/batch', { method: 'POST', headers: { Authorization: `Basic ${auth}`, 'X-Api-Key': process.env.FM_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ "from": "FASTERMSG", "content": "Your delivery arrives tomorrow between 9 am and 12 pm.", "messages": [ { "to": "+22960000000", "client_reference": "cmd-42" }, { "to": "+22961000001", "client_reference": "cmd-43" }, { "to": "+22962000002", "content": "Your delivery arrives today.", "client_reference": "cmd-44" } ] }), }); const data = await response.json(); console.log(response.status, data);
<?php $ch = curl_init('https://api-next.fastermessage.com/v2/sms/batch'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_USERPWD => getenv('FM_API_USER') . ':' . getenv('FM_API_SECRET'), CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('FM_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'from' => 'FASTERMSG', 'content' => 'Your delivery arrives tomorrow between 9 am and 12 pm.', 'messages' => [ [ 'to' => '+22960000000', 'client_reference' => 'cmd-42', ], [ 'to' => '+22961000001', 'client_reference' => 'cmd-43', ], [ 'to' => '+22962000002', 'content' => 'Your delivery arrives today.', 'client_reference' => 'cmd-44', ], ], ]), ]); $response = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $status . PHP_EOL . $response . PHP_EOL;
import os import requests response = requests.post( "https://api-next.fastermessage.com/v2/sms/batch", auth=(os.environ["FM_API_USER"], os.environ["FM_API_SECRET"]), headers={"X-Api-Key": os.environ["FM_API_KEY"]}, json={ "from": "FASTERMSG", "content": "Your delivery arrives tomorrow between 9 am and 12 pm.", "messages": [ { "to": "+22960000000", "client_reference": "cmd-42", }, { "to": "+22961000001", "client_reference": "cmd-43", }, { "to": "+22962000002", "content": "Your delivery arrives today.", "client_reference": "cmd-44", }, ], }, timeout=15, ) print(response.status_code, response.json())
{
"total": 3,
"accepted": 2,
"rejected": 1,
"data": [
{ "index": 0, "id": "msg_2f1c9d4e8a6b", "status": "queued", "to": "22960000000" },
{ "index": 1, "id": "msg_7d3b5e0c1a84", "status": "queued", "to": "22961000001" }
],
"errors": [
{
"index": 2,
"code": "invalid_number",
"message": "The recipient number is not valid.",
"param": "to",
"to": "2296"
}
]
}Message status
Returns the message resource, in the exact shape that sending returns: one deserialiser is enough on both sides.
Possible statuses
| Status | Terminal | Meaning |
|---|---|---|
| scheduled | no | Scheduled, waiting for its send date. |
| queued | no | Accepted and queued, not yet handed to the carrier. |
| sent | no | Handed to the carrier, delivery receipt not received. |
| delivered | yes | Delivered to the recipient, confirmed by the carrier. |
| undelivered | yes | Not delivered — number switched off, out of service, mailbox full. |
| failed | yes | Failure before delivery: carrier rejection, content refused, gateway error. |
curl https://api-next.fastermessage.com/v2/messages/msg_2f1c9d4e8a6b \ -u "$FM_API_USER:$FM_API_SECRET" \ -H "X-Api-Key: $FM_API_KEY"
const auth = Buffer.from(`${process.env.FM_API_USER}:${process.env.FM_API_SECRET}`).toString('base64'); const response = await fetch('https://api-next.fastermessage.com/v2/messages/msg_2f1c9d4e8a6b', { method: 'GET', headers: { Authorization: `Basic ${auth}`, 'X-Api-Key': process.env.FM_API_KEY, }, }); const data = await response.json(); console.log(response.status, data);
<?php $ch = curl_init('https://api-next.fastermessage.com/v2/messages/msg_2f1c9d4e8a6b'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_USERPWD => getenv('FM_API_USER') . ':' . getenv('FM_API_SECRET'), CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('FM_API_KEY'), ], ]); $response = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $status . PHP_EOL . $response . PHP_EOL;
import os import requests response = requests.get( "https://api-next.fastermessage.com/v2/messages/msg_2f1c9d4e8a6b", auth=(os.environ["FM_API_USER"], os.environ["FM_API_SECRET"]), headers={"X-Api-Key": os.environ["FM_API_KEY"]}, timeout=15, ) print(response.status_code, response.json())
{
"id": "msg_2f1c9d4e8a6b",
"channel": "sms",
"status": "delivered",
"to": "22960000000",
"from": "FASTERMSG",
"units": 1,
"price": 12,
"currency": "XOF",
"refund": null,
"client_reference": "cmd-42",
"fallback_of": null,
"error": null,
"created_at": "2026-09-07T10:00:00.000Z",
"scheduled_at": null,
"sent_at": "2026-09-07T10:00:01.420Z",
"delivered_at": "2026-09-07T10:00:06.900Z"
}List messages
Query parameters
| Field | Type | Description |
|---|---|---|
| channeloptional | string | Filter on one channel: sms, whatsapp, email, voice. |
| statusoptional | string | Filter on one of the six statuses. |
| limitoptional | integer | Page size. |
| cursoroptional | string | Pagination cursor — pass back the next_cursor value of the previous page. |
{
"data": [ { "id": "msg_2f1c9d4e8a6b", "status": "delivered" } ],
"next_cursor": "eyJvIjoxfQ",
"has_more": true
}Balance
Returns the available balance, channel by channel and in the account currency. Every channel is returned, including at zero: a missing channel would be indistinguishable from an empty balance.
curl https://api-next.fastermessage.com/v2/balance \ -u "$FM_API_USER:$FM_API_SECRET" \ -H "X-Api-Key: $FM_API_KEY"
const auth = Buffer.from(`${process.env.FM_API_USER}:${process.env.FM_API_SECRET}`).toString('base64'); const response = await fetch('https://api-next.fastermessage.com/v2/balance', { method: 'GET', headers: { Authorization: `Basic ${auth}`, 'X-Api-Key': process.env.FM_API_KEY, }, }); const data = await response.json(); console.log(response.status, data);
<?php $ch = curl_init('https://api-next.fastermessage.com/v2/balance'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_USERPWD => getenv('FM_API_USER') . ':' . getenv('FM_API_SECRET'), CURLOPT_HTTPHEADER => [ 'X-Api-Key: ' . getenv('FM_API_KEY'), ], ]); $response = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); echo $status . PHP_EOL . $response . PHP_EOL;
import os import requests response = requests.get( "https://api-next.fastermessage.com/v2/balance", auth=(os.environ["FM_API_USER"], os.environ["FM_API_SECRET"]), headers={"X-Api-Key": os.environ["FM_API_KEY"]}, timeout=15, ) print(response.status_code, response.json())
{
"balances": [
{ "channel": "sms", "amount": 289009, "currency": "XOF" },
{ "channel": "whatsapp", "amount": 12000, "currency": "XOF" },
{ "channel": "email", "amount": 0, "currency": "XOF" },
{ "channel": "voice", "amount": 4500, "currency": "XOF" }
]
}Credit movements
Movement log: credit (purchase), debit (usage) and refund, kept distinct from a credit — a system refund is not a purchase.
Senders (Sender ID)
What the API does impose, on every send: from is mandatory on the SMS channel and must match a Sender ID approved on the account. There is no per-account default sender.
| Situation | Behaviour |
|---|---|
from missing | Refused with 400 missing_parameter, before any balance check. |
from unknown or not approved | Refused with 403. Nothing is charged. |
| Account in test mode | The brand’s default Sender ID can be borrowed, with no approval of its own. |
How to declare one is set out in the back-office documentation.
The rest of the surface
The endpoints below follow the same conventions for authentication, errors and pagination.
| Method | Path | Purpose |
|---|---|---|
| POST | /v2/messages | Unified sending — the channel is a body parameter. |
| POST | /v2/whatsapp/send | WhatsApp send through a Meta-approved template. |
| POST | /v2/email/send | Email send — content is an object with a subject. |
| POST | /v2/voice/send | Voice call — spoken text or a pre-recorded audio_url. |
| POST | /v2/otp | Issue a verification code. |
| POST | /v2/otp/verify | Verify a code. |
| GET | /v2/contacts | Address book — read, create, update, delete. |
| GET | /v2/contact-groups | Contact groups, same operations. |
| GET | /v2 | Contract discovery, without credentials. |
| GET | /v2/ping | Connectivity test, without credentials. |
Webhooks and delivery receipts
A message’s status changes after the HTTP response. Rather than polling GET /v2/messages/{id}, declare a callback URL: it is called with POST on every state change.
The URL is set either on the account (back office, Integration & API) or per message through callback_url — which then takes precedence over the account one.
{
"id": "msg_2f1c9d4e8a6b",
"channel": "sms",
"status": "delivered",
"to": "22960000000",
"from": "FASTERMSG",
"units": 1,
"price": 12,
"currency": "XOF",
"client_reference": "cmd-42",
"error": null,
"sent_at": "2026-09-07T10:00:01.420Z",
"delivered_at": "2026-09-07T10:00:06.900Z"
}Statuses notified
The same six as on the message resource. Only delivered, undelivered and failed are terminal: after them, no further callback is to be expected for that message.
Retry policy
- Your endpoint must answer
2xxin under ten seconds. Any other response, or an overrun, counts as a failure. - A failure is replayed with a growing interval. So answer
200on receipt and process afterwards: slow processing always ends up being replayed. - Callbacks are not ordered. A
sentcan arrive after adelivered— trust the timestamps, never the order of arrival. - The same event may be delivered twice. Make your processing idempotent on the
id+statuspair.
Security
Never put a destructive action behind the callback URL, and serve it over HTTPS only.
Error codes
Every error follows the same shape, whatever the endpoint: a single object under error, which a client can tell apart from a success while knowing nothing about the call.
{
"error": {
"type": "invalid_request",
"code": "missing_parameter",
"message": "A recipient is required.",
"param": "to",
"request_id": "2f1c9d4e-8a6b-4c1e-9f2a-7d3b5e0c1a84"
}
}type says how to react, code says what happened. Test code literally: it is stable. Never test message, which is written for developers and may be rephrased.
Error families
| type | HTTP | How to react |
|---|---|---|
| invalid_request | 400 · 404 · 409 | Fix the call. Do not retry it unchanged. |
| authentication_error | 401 | Credentials missing, wrong or revoked. |
| permission_error | 403 | Authenticated, but the key lacks the required scope. |
| business_error | 402 · 403 | Business refusal: neither the call nor the credentials are at fault. |
| rate_limit_error | 429 | Retry after the Retry-After delay. |
| api_error | 500 · 501 · 503 | A failure on our side. Retryable, and never charged. |
Common codes
| code | HTTP | Cause | Resolution |
|---|---|---|---|
| missing_parameter | 400 | A mandatory field is missing; param names it. | Add the field reported. |
| invalid_parameter | 400 | Field present but unreadable: non-ISO date, unknown channel, content of the wrong type. | Fix the format; see the channel’s example. |
| invalid_number | 400 | Recipient outside E.164 format, or not routable. | Normalise to E.164 (8 to 15 digits). |
| unauthenticated | 401 | Basic or X-Api-Key missing, wrong, revoked, or the two with no affiliation between them. | Check both credentials; regenerate the key if needed. |
| insufficient_scope | 403 | The key is not allowed to perform this action. | Use a key of the account concerned. |
| insufficient_balance | 402 | Available balance too low, current holds deducted. | Top up the paying account. |
| recipient_opted_out | 403 | The recipient removed themselves from the channel’s “Do not contact” list. | Stop contacting them on this channel; the list is per channel. |
| sender_not_allowed | 403 | Sender ID unknown to the account, or not approved. | Declare the sender and have it approved. |
| resource_not_found | 404 | Unknown identifier — or one belonging to another account. | Check the id and the key’s account. |
| idempotency_conflict | 409 | The same Idempotency-Key was used for a different payload. | Use a new key. |
| idempotency_in_progress | 409 | A request carrying this key is still in flight. | Wait, then replay unchanged. |
| rate_limit_exceeded | 429 | Rate limit reached. | Wait for Retry-After, then retry. |
| not_implemented | 501 | Endpoint declared in the contract, not served yet. | Nothing to fix — follow us for its release. |
| service_unavailable | 503 | Temporary unavailability. | Retry with a growing interval. |
| internal_error | 500 | Unexpected failure on our side. | Retry, then quote us the request_id. |
Rate limits
Throughput is capped per API key, over a sliding window. The default cap is 600 requests per minute; it can be raised on request, according to your volume.
Response headers
| Header | Content |
|---|---|
| X-RateLimit-Limit | Cap for the current window. |
| X-RateLimit-Remaining | Calls left before refusal. |
| X-RateLimit-Reset | Unix timestamp (seconds) when the window resets. |
| Retry-After | Seconds to wait before retrying — present on a 429 only. |
A well-behaved client paces itself on X-RateLimit-Remaining rather than waiting for the refusal. On a 429, honour Retry-After: retrying sooner only prolongs the block.
Other caps
- 500 lines per batch send.
- 24 h of retention for an
Idempotency-Key; past that, the key is reusable and the call goes through again. - 10 s for a webhook endpoint to answer.