# DDG SMS Gateway — API integration reference > Send and receive SMS through https://smsgateway.ddg.mx. This page is > generated live from the gateway's configuration (2026-07-15T04:30:29Z) — the > routes listed below exist right now. Re-fetch it rather than caching. > > Human-readable version: https://smsgateway.ddg.mx/api-docs ## Authentication Every platform request needs an API key in the `X-API-Key` header (format `sg_…`, issued in the gateway admin panel). Store it in your project `.env`: ```dotenv SMSGW_API_BASE=https://smsgateway.ddg.mx/api/v1 SMSGW_API_KEY=sg_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` All responses are JSON: `{"status": "success"|"error", "message": …, "data": …}`. ## Live routes A **route** is a SIM on one of the gateway's phones — every enrolled SIM is listed below, one row per slot. Pass a route's label as the `sim` field when sending to pick which number the message goes out on; unlabeled SIMs can't be targeted directly but still carry `"any"`/default traffic. Current routes: | route (`sim` value) | sim slot | carrier | hourly cap | usable | device online | |---|---|---|---|---|---| | _unlabeled — via `"any"`/default only_ | 0 | T-Mobile Wi-Fi Calling | 25/hr | yes | yes | | `bg-primary` | 1 | T-Mobile | 25/hr | yes | yes | Routing resolution order for a send: 1. Explicit `sim` value — matched against route label, then phone number. Unknown or disabled routes fail loudly with `422`. 2. Account default route (set in admin) — unavailable default also fails with `422` rather than silently rerouting. 3. `"any"` (or no default set) — least-loaded pick among usable routes on online devices, ranked by remaining hourly headroom. Devices are considered offline after 10 minutes without a heartbeat. Online devices right now: **1**. Per-route hourly caps protect numbers from carrier spam-flagging; when every route is at cap or offline, sends return `503`. ## Endpoints ### `POST /api/v1/messages` Enqueue an outbound SMS. Returns 201 with the message row, or 200 replaying the original when client_ref matches a prior send (idempotent). - `to` — required — destination number, E.164 (bare 10-digit US numbers are auto-prefixed +1) - `body` — required — message text; long bodies are split into parts automatically - `sim` — optional — a route label from the routes table, a route phone number, or "any" for least-loaded; omit for the account default - `client_ref` — optional — idempotency key (max 64 chars); re-POSTing the same value returns the original message instead of sending twice ### `GET /api/v1/messages?id=N` Fetch one message (status polling). ### `GET /api/v1/messages` List messages, newest first, paged. - `status` — optional — filter: pending|pushed|sending|sent|delivered|failed|canceled - `to` — optional — filter by destination number - `since` — optional — created_at >= this datetime (server time, America/New_York) - `page` — optional — 1-based page number - `per_page` — optional — default 50, max 200 ### `POST /api/v1/messages?id=N&action=cancel` Cancel a message that is still queued (pending/pushed). Already-sending messages cannot be canceled. ### `GET /api/v1/status` Preflight: 24h queue counts for your account, your default route with online state, and the number of online devices. ### `GET /api/v1/inbound` Pull-style inbound SMS list for your account (push-style delivery happens via your webhook), newest first, paged. - `since` — optional — created_at >= this datetime - `from` — optional — filter by sender number - `page` — optional — 1-based page number - `per_page` — optional — default 50, max 200 ## Sending — example ``` POST /api/v1/messages X-API-Key: sg_… Content-Type: application/json {"to": "+15551234567", "body": "Your code is 123456", "sim": "any", "client_ref": "otp-9f83a1"} ``` Response `201`: ```json {"status": "success", "data": {"id": 42, "status": "pending", "route": "any", "to": "+15551234567", ...}} ``` Message status walks `pending → pushed → sending → sent → delivered` (or exits to `failed` / `canceled`). Poll `GET /api/v1/messages?id=42`, or subscribe to `status` webhook events. Always send a `client_ref` for OTPs and transactional messages — retrying a timed-out POST with the same `client_ref` can never double-send. ## Inbound SMS — webhooks Configure a webhook URL + signing secret on your account in admin and tick the events you want (`inbound`, `status`). Each event is POSTed as JSON with these headers: ``` X-SG-Event: inbound_sms X-SG-Signature: sha256= X-SG-Delivery: 123 ``` Verify the signature before trusting the body — HMAC-SHA256 of the raw request body with your signing secret: ```php $raw = file_get_contents('php://input'); $expected = 'sha256=' . hash_hmac('sha256', $raw, $_ENV['SMSGW_WEBHOOK_SECRET']); if (!hash_equals($expected, $_SERVER['HTTP_X_SG_SIGNATURE'] ?? '')) { http_response_code(401); exit; } $event = json_decode($raw, true); // handle $event['data'] … http_response_code(200); ``` Return `2xx` quickly. Non-2xx deliveries are retried with backoff (1m, 5m, 15m, 1h, 6h); after all retries the delivery is marked failed (visible in admin → Inbound). You can also pull inbound without a webhook: `GET /api/v1/inbound?since=…`. ## HTTP status codes - `200` — OK (idempotent replay on send; success on reads) - `201` — Message created - `401` — Invalid or missing X-API-Key - `404` — Not found (or not your account's resource) - `405` — Method not allowed - `422` — Invalid number, empty body, or unknown/disabled route - `429` — Account daily quota exceeded - `503` — No online device/SIM available ## PHP client (drop-in) ```php function smsgw_send(string $to, string $body, ?string $sim = null, ?string $clientRef = null): array { $ch = curl_init(rtrim($_ENV['SMSGW_API_BASE'], '/') . '/messages'); $payload = ['to' => $to, 'body' => $body]; if ($sim) $payload['sim'] = $sim; if ($clientRef) $payload['client_ref'] = $clientRef; curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'X-API-Key: ' . $_ENV['SMSGW_API_KEY'], ], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, ]); $raw = curl_exec($ch); $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return ['ok' => $status >= 200 && $status < 300, 'status' => $status, 'body' => json_decode($raw, true)]; } ``` ## Notes for agents - Timestamps in API responses (`created_at`, `sent_at`, …) are UTC in `Y-m-d H:i:s` format; `since` filter values are interpreted as UTC too. - Numbers are normalized to E.164; bare 10-digit US numbers get `+1`. - Check `GET /api/v1/status` before bulk sends: it returns your queue depth, your default route's online state, and online device count. - This gateway fronts physical Android phones — delivery capacity is finite. Respect the hourly caps shown in the routes table.