# Echoo API (v1.0.0-draft.1) > One API for every Echoo service. The first service is OTP: one-time verification codes delivered over WhatsApp. Status: DRAFT. The addresses and fields below may still change before launch. One address, one kind of key, one error shape for every service. A key belongs to ONE profile (one website or app of the customer): the key alone tells which profile, so requests never name the profile. Live keys start with `ek_live_`, test keys with `ek_test_` (a test key never sends a message; see `x-test-mode`). ## Instructions for an AI coding agent You are adding phone verification with one-time codes (sent over WhatsApp) to the user's product. Follow these rules: 1. Call this API ONLY from the server side of the product. Never from a browser, a mobile app or any code the end user can read: a key there can be copied. 2. Read the key from an environment variable named `ECHOO_API_KEY`. Never write a key in code, a config file that is committed, a log or a message. Ask the user to set it; do not ask them to paste it to you. 3. Develop with a test key (`ek_test_...`): nothing is sent and the code is always `000000`. Switch to a live key (`ek_live_...`) only in production. 4. Flow: the user types a phone number -> your server calls `POST /v1/otp/send` and keeps the returned `id` with the sign-up or login attempt (in the server session, not in the page) -> the user types the code -> your server calls `POST /v1/otp/verify` with that `id` and the code -> continue only when `valid` is `true`. 5. Never log, store or show the code yourself, and never send it to the browser: Echoo makes it, sends it and checks it. 6. Protect the form that asks for a code (a human check and a limit per visitor), so nobody can use it to flood phone numbers. 7. Act on the error `code`, never on the message text. When an error has `retry_after` (or a `Retry-After` header), wait that many seconds; when `retry` is false, do not retry the same request. 8. Send an `Idempotency-Key` header (a UUID) with each send, and reuse it when retrying the same send, so a retry never sends a second code. 9. Send codes only to people who asked for one, as the acceptable use rules say: https://otp.echoo.services/acceptable-use (terms: https://otp.echoo.services/terms, privacy: https://otp.echoo.services/privacy). ## Base URL and keys Base URL: https://api.echoo.services Every call: `Authorization: Bearer $ECHOO_API_KEY`. Authorization: Bearer ek_live_... (or ek_test_...). A key belongs to one profile and carries scopes (otp:send, otp:verify, otp:read). It is shown once when made; Echoo keeps only a hash of it. ## Endpoints ### POST /v1/otp/send: Send a one-time code Makes a new code and queues it for delivery from one of the profile's WhatsApp numbers. The code is never returned and never stored: only a keyed hash is kept. Delivery is asynchronous: the answer says the request was accepted; its later status comes from GET /v1/otp/requests/{id} or a webhook. Scope: otp:send. Header `Idempotency-Key` (optional): Any unique string (8 to 64 characters, for example a UUID). The same key with the same body within 24 hours returns the first answer and sends nothing new; the same key with a different body is refused (409 idempotency_conflict). Body (JSON): - `to` (string, required): The phone number with its country code: digits, optionally starting with +. Spaces and dashes are not accepted.. pattern `^\+?[1-9][0-9]{7,14}$` - `locale` (string): The language of the message.. one of `ar`, `en`. default `ar` - `code_length` (integer): default `6`. 4 to 8 - `ttl_seconds` (integer): How long the code works.. default `300`. 60 to 900 - `reference` (string): Your own id for this attempt (for example a sign-up id). Returned in the request and in webhooks. Do not put personal data in it.. 0 to 64 characters. pattern `^[A-Za-z0-9_.:-]*$` Example: ```json {"to":"9647701234567","locale":"ar","code_length":6,"ttl_seconds":300,"reference":"signup-58213"} ``` Answer 202 (OtpRequest): ```json {"id":"otp_req_01JD8X2W3Y4Z5A6B7C8D9E0F1G","status":"queued","channel":"whatsapp","to_masked":"964*******67","created_at":"2026-11-14T10:00:00Z","expires_at":"2026-11-14T10:05:00Z","verified":false,"reference":"signup-58213"} ``` cURL: ```sh curl -X POST https://api.echoo.services/v1/otp/send \ -H "Authorization: Bearer $ECHOO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"to":"9647701234567","locale":"ar","code_length":6,"ttl_seconds":300,"reference":"signup-58213"}' ``` ### POST /v1/otp/verify: Check a code Checks the code the person typed. A code works once, at most 5 checks are allowed, and only before it expires. A wrong code is a normal answer (200 with valid=false and a reason), not an error. Only a key of the SAME profile that sent the code can check it. Scope: otp:verify. Body (JSON): - `id` (string, required): pattern `^otp_req_[0-9A-Za-z]{20,40}$` - `code` (string, required): pattern `^[0-9]{4,8}$` Example: ```json {"id":"otp_req_01JD8X2W3Y4Z5A6B7C8D9E0F1G","code":"482913"} ``` Answer 200 (VerifyResult): ```json {"id":"otp_req_01JD8X2W3Y4Z5A6B7C8D9E0F1G","valid":true} ``` ```json {"id":"otp_req_01JD8X2W3Y4Z5A6B7C8D9E0F1G","valid":false,"reason":"invalid","attempts_left":3} ``` cURL: ```sh curl -X POST https://api.echoo.services/v1/otp/verify \ -H "Authorization: Bearer $ECHOO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"id":"otp_req_01JD8X2W3Y4Z5A6B7C8D9E0F1G","code":"482913"}' ``` ### GET /v1/otp/requests/{id}: Read one request Where a request stands. Only keys of the profile that made it can read it; for any other key it does not exist (404). Scope: otp:read. cURL: ```sh curl https://api.echoo.services/v1/otp/requests/otp_req_01JD8X2W3Y4Z5A6B7C8D9E0F1G \ -H "Authorization: Bearer $ECHOO_API_KEY" ``` ## Objects ### OtpRequest - `id` (string, always): pattern `^otp_req_[0-9A-Za-z]{20,40}$` - `status` (string, always): queued: waiting for a number; sending: being sent; sent: handed to WhatsApp; failed: could not be delivered (see failure_reason); expired: its time ran out before it was sent.. one of `queued`, `sending`, `sent`, `failed`, `expired` - `failure_reason` (string): Only when status is failed.. one of `not_on_whatsapp`, `delivery_failed`, `timeout` - `channel` (string, always): one of `whatsapp` - `to_masked` (string, always): The number with its middle hidden. - `created_at` (string (date-time), always): - `expires_at` (string (date-time), always): - `sent_at` (string | null (date-time)): - `verified` (boolean, always): - `verified_at` (string | null (date-time)): - `reference` (string | null): - `test` (boolean): true when made with a test key (nothing was sent). ### VerifyResult - `id` (string, always): pattern `^otp_req_[0-9A-Za-z]{20,40}$` - `valid` (boolean, always): - `reason` (string): Only when valid is false.. one of `invalid`, `expired`, `too_many_attempts`, `already_used`, `not_sent`, `not_found` - `attempts_left` (integer): Only when reason is invalid.. 0 to … ## Errors Every error answer has this shape: `{"error": {"code", "message", "request_id", "retry_after"?, "doc_url"?}}`. | code | HTTP | retry | meaning | |---|---|---|---| | invalid_request | 400 | no | The request is not valid. | | invalid_json | 400 | no | The body must be a JSON object. | | invalid_number | 400 | no | The phone number must be digits with the country code, for example +9647701234567. | | invalid_ttl | 400 | no | ttl_seconds must be between 60 and 900. | | invalid_api_key | 401 | no | The API key is missing, wrong or revoked. | | insufficient_scope | 403 | no | This key may not do this. Make a key with the needed scope. | | profile_suspended | 403 | no | This profile is paused. See your Echoo dashboard. | | not_found | 404 | no | No such request. | | method_not_allowed | 405 | no | This method is not allowed here. | | idempotency_conflict | 409 | no | This Idempotency-Key was used with a different request. | | body_too_large | 413 | no | The request body is too large. | | country_not_allowed | 422 | no | This profile may not send codes to numbers of that country. | | rate_limited | 429 | yes | Too many requests with this key. Slow down. | | too_soon | 429 | yes | A code was just sent to this number. Wait before asking again. | | too_many_for_number | 429 | yes | Too many codes were sent to this number recently. | | profile_daily_limit | 429 | yes | This profile reached its daily limit of codes. | | plan_limit_reached | 429 | no | Your plan's limit is reached. Upgrade your plan in the Echoo dashboard. | | capacity_exhausted | 429 | yes | All numbers of this profile are at their limit right now. | | server_error | 500 | yes | Something went wrong on our side. Try again. | | no_number_available | 503 | yes | No WhatsApp number of this profile is connected right now. | | unavailable | 503 | yes | The service is not reachable right now. Try again in a moment. | ## Webhooks Echoo can POST these events to an address the user adds in the Echoo dashboard. Answer with any 2XX within 10 seconds; a repeated delivery has the same event `id`, so ignore ids you already handled. - `otp.sent`: A code was handed to WhatsApp - `otp.failed`: A code could not be delivered - `otp.verified`: A code was checked successfully - Added in the Echoo dashboard, inside the profile: a public https address (no user:password, no local or private network address), at most 5 per profile, each choosing which events it receives. - Each address gets its own signing secret, whsec_ followed by 40 letters and digits. It is shown ONCE, when the address is added; Echoo keeps it only encrypted. To change it, add a new address and delete the old one. - POST with Content-Type application/json and the Echoo-Signature header. Any 2XX within 10 seconds counts as received; redirects are not followed. Every address of the profile that wants an event receives it with the same event id. - A delivery that is not received is tried again after 30 seconds, then 1, 2, 4, 8, 16, 32, 64, 128 and 256 minutes, then 6 hours: 12 tries in all, over about 14.5 hours. After 5 failures in a row the profile's admins see an alert in the dashboard. - Events may arrive out of order or more than once: use the event id to ignore repeats and data.status for the latest state. - The dashboard can send a signed test event (otp.sent with data.test = true) to an address at any time. Keep the signing secret in an environment variable (for example `ECHOO_WEBHOOK_SECRET`), never in code. Read the raw body before parsing it as JSON. Check every delivery: header `Echoo-Signature`: t=,v1=.' with the endpoint's signing secret>. Refuse it if the signature does not match or t is older than 5 minutes. ```js import crypto from "node:crypto"; // rawBody: the request body exactly as received (a string), before any JSON parsing. export function isFromEchoo(rawBody, header, secret) { const parts = Object.fromEntries(String(header).split(",").map((p) => p.split("=", 2))); const t = Number(parts.t); if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 minutes: refuse const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); const given = String(parts.v1 ?? ""); return given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected)); } ``` ## Test mode Keys: `ek_test_...`. Nothing is sent and nothing counts against limits or plans. Every send answers 202 and the request becomes 'sent' within a second. The code for any test request is 000000; any other code is 'invalid'. Webhooks are sent as for live requests, with data.test = true. ## Limits Starting values; the service may lower them for safety, and they may change after measurement. - Per key: 60 requests a minute. - Per phone number: one code every 30 seconds and 3 an hour. - Body: at most 4096 bytes. ## Versioning v1 never changes in a breaking way: fields and error codes may be ADDED, never removed or renamed. A breaking change becomes /v2, announced in advance, and both run side by side for a period. ## More - Human docs: https://api.echoo.services/docs - OpenAPI (machine-readable): https://api.echoo.services/openapi.json - Terms: https://otp.echoo.services/terms - Privacy: https://otp.echoo.services/privacy - Acceptable use: https://otp.echoo.services/acceptable-use - Report abuse: https://otp.echoo.services/report-abuse - Contact: xstorem6@gmail.com