From your first send to a signed webhook.
One REST API for WhatsApp, SMS, RCS, email and push, with test and live keys, safe retries and signed events. The whole public contract lives in openapi.json.
Test keys start with ccx_test_ and run on a test wallet. Nothing reaches a customer.
- Create an API keyccx_test_••••••••Shown once
- Send the messagePOST /messages201 queued
- Get the status on your webhookmessage.deliveredSignature valid
Your first send, in three steps.
Start with a test key: it uses the test wallet and a simulated send, so nothing reaches any customer and nothing is really charged.
curl -X POST https://api.messages.ccxcompany.co/messages \ -H "Authorization: Bearer $CCX_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-4821-shipped" \ -d '{ "numberId": "num_…", "to": "+14155550123", "type": "text", "payload": { "text": { "body": "Your order is out for delivery" } } }'
Body shortened; the full message object is in openapi.json. Values with … are placeholders.
Keys, scopes and safe retries.
Every call carries Authorization: Bearer with your key. Each route requires a scope, and a key can only do what its scopes allow.
Retry without sending twice.
On WhatsApp, SMS and push sends, the Idempotency-Key header lets you repeat the same call for 24 hours. Same key and same body return the original response. Same key with a different body returns 409. Try it.
- Idempotency-Key: order-4821 · body A201msg_01… created and queued
- Idempotency-Key: order-4821 · body A201Original response for msg_01…, nothing sent again.
- Idempotency-Key: order-4821 · body B409CONFLICT · IDEMPOTENCY_CONFLICT
Simulated responses, abbreviated. Values with … are placeholders.
Signed webhooks, with redelivery.
Register the URL, pick the events and keep the secret. Every delivery carries X-CCX-Signature, X-CCX-Event, X-CCX-Delivery-Id and X-CCX-Idempotency-Key.
- 5 s
- 30 s
- 2 min
- 10 min
- 1 h
import { createHmac, timingSafeEqual } from "node:crypto"; // X-CCX-Signature: t=<unix>,v1=<hex>export function validSignature(secret, body, header) { const p = Object.fromEntries( header.split(",").map((kv) => kv.split("="))); if (Math.abs(Date.now() / 1000 - p.t) > 300) return false; const expected = createHmac("sha256", secret) .update(p.t + "." + body).digest("hex"); return expected.length === p.v1.length && timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(p.v1, "hex"));}
Compute it over the raw body, exactly as received, without re-serializing the JSON. The 5-minute window on the t stamp blocks replays of old events.
- message.sent
- message.delivered
- message.read
- message.failed
- message.received
- template.approved
- template.rejected
- sms.delivered
- sms.received
- email.delivered
- email.bounced
- email.opened
- wallet.low_balance
When the response isn't 2xx.
Every error comes as problem details (RFC 7807), with a stable code your own code can compare against. Pick one to see its response.
HTTP/1.1 409Content-Type: application/problem+json { "type": "about:blank", "title": "conflict", "code": "CONFLICT", "status": 409, "detail": "idempotency key reused with a different payload", "meta": { "code": "IDEMPOTENCY_CONFLICT" }}
Sample error body, as the API returns it.
What you can do through the API.
The routes below are part of the public contract. Anything not in openapi.json isn't contract and may change.
Guides for what gets stuck most.
Short answers to the questions that come up once the integration leaves the whiteboard.
Accents swapped before sending: á becomes a. The whole message fits in GSM-7, so it stays at 160 characters for one segment.
Counted the way the bill counts. POST /v2/sms/preview returns segments and price before sending. SMS goes to Brazilian numbers; international routes are on request.
Meta rejected my template. Now what?
It's almost always format, not content: a promotion declared as utility, a variable at the start or end of the body, or a URL button without a fixed prefix. Before you submit to Meta, the dashboard editor warns when the text uses aggressive promotional language, when a marketing template has no opt-out button and when the body goes over 1,024 characters.
How long does number approval take?
There are two steps. Number verification, with the code by SMS or call, is quick. Business verification, which unlocks higher sending limits, is done by Meta itself, and the timeline is Meta's.
How many segments will my SMS cost?
Up to 160 characters is one segment; beyond that, one every 152. With emoji the message goes as Unicode: up to 70 is one segment, beyond that one every 67. Accented characters missing from GSM-7 are swapped for their plain equivalent when the whole message fits in GSM-7, so accents don't raise the price. POST /v2/sms/preview returns segments and price before sending. SMS goes to Brazilian numbers; international routes are on request.
What happens if my balance runs out?
The wallet is prepaid and is charged at send time. With no balance, SMS, RCS and email return 402, and WhatsApp and push sends fail with an insufficient-balance status. An SMS campaign that runs out mid-send pauses with the reason on screen, and Resume sends the messages refused for lack of balance along with the rest. The test wallet is separate and never charges real money.
My emails are landing in spam
Publish SPF, DKIM and DMARC for the domain, ramp up volume slowly on a new domain, remove invalid addresses and keep transactional and marketing on separate subdomains. Addresses that bounce go onto the suppression list.
My webhook isn't receiving events
Check that the URL answers 2xx within 10 seconds, that the signature is computed over the raw body and that the webhook is active, not paused. A 4xx from your side (other than 408 and 429) isn't retried; 5xx and no answer are, after 5 s, 30 s, 2 min, 10 min and 1 h.
How do I invite my team and set permissions?
Invite by email and pick one of nine roles, from owner to read-only. The organization can require two-step verification from every member, with a 30-day grace period. There's no per-seat fee, so nobody needs to share a login.
Stuck on a route? Talk to the people who run the API.
Sign up through the site and get a US$5 test credit.