SMS Orchestrator — API Contracts
Status: populated Owner: Platform Engineering Last updated: 2026-04-18 Companion: APPLICATION_LOGIC · ADR-0001 Kong
All endpoints are fronted by Kong; clients use the Kong base URL. Auth is enforced by Kong (JWT or API key); this service additionally re-checks X-Tenant-Id against JWT claim.
1. Endpoints
1.1 POST /v1/sms/send
Accept a single outbound SMS.
Headers (required):
Authorization: Bearer <jwt>ORX-Api-Key: <key>(validated by Kong)Content-Type: application/jsonIdempotency-Key: <client-key>— recommended; 48h replay window
Headers (injected by Kong):
X-Request-Id,traceparent,X-Tenant-Id,X-Api-Key-Id
Request body:
{
"to": "+447700900123",
"from": "+447700900999",
"body": "Your code is 123456",
"messageType": "SMS",
"callbackUrl": "https://tenant.example.com/webhooks/sms",
"metadata": { "campaignId": "camp-001" }
}
Response 202:
{
"messageId": "018e7c3a-9b2f-7d4e-a5f1-3c2b1a0d9e8f",
"status": "QUEUED",
"segmentCount": 1,
"enqueuedAt": "2026-04-18T10:00:00.000Z"
}
Errors:
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_PAYLOAD | Zod schema failure |
| 400 | INVALID_DESTINATION | to not E.164 |
| 400 | SMS_BODY_TOO_LONG | body > 1600 chars |
| 401 | UNAUTHENTICATED | Kong rejected (not reached this service) |
| 403 | TENANT_MISMATCH | JWT tenant ≠ X-Tenant-Id header |
| 409 | IDEMPOTENCY_CONFLICT | Same key + different body |
| 413 | REQUEST_TOO_LARGE | Kong body size exceeded |
| 429 | RATE_LIMITED | Kong rate-limit-advanced (not reached this service) |
| 503 | DEPENDENCY_UNAVAILABLE | Redis or PG down |
1.2 POST /v1/sms/bulk
Same payload schema, wrapped in { messages: [...] }. Max 500. Returns { accepted: [...], rejected: [...] }.
1.3 GET /v1/sms/{messageId}
Response 200: { messageId, status, attemptCount, operatorId, routeId, enqueuedAt, processedAt, lastError }.
404 if not found or not in caller's account scope.
1.4 GET /health/live, /health/ready, /metrics
Operational endpoints; not exposed via Kong.
2. Error Response Shape (problem+json)
{
"type": "https://errors.ghasi.io/sms/INVALID_DESTINATION",
"title": "Invalid destination phone number",
"status": 400,
"code": "INVALID_DESTINATION",
"detail": "Value '00447700900123' is not a valid E.164 number",
"instance": "/v1/sms/send",
"requestId": "req_01H..."
}
3. OpenAPI
Generated from NestJS decorators into openapi.json and committed. Kong route config is linted in CI against the generated OpenAPI.
4. Idempotency Contract
- Hash =
sha256(accountId || ":" || idempotencyKey). - Window: 48 hours.
- Replay: identical body → same
messageId+ 202. Different body → 409IDEMPOTENCY_CONFLICT. - Missing key: allowed; no replay protection.
5. Pagination (GET /v1/sms — future)
Cursor-only: ?cursor=<opaque>&limit=<1..100>. Link: <...>; rel="next".