Skip to main content

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> OR X-Api-Key: <key> (validated by Kong)
  • Content-Type: application/json
  • Idempotency-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:

HTTPCodeMeaning
400INVALID_PAYLOADZod schema failure
400INVALID_DESTINATIONto not E.164
400SMS_BODY_TOO_LONGbody > 1600 chars
401UNAUTHENTICATEDKong rejected (not reached this service)
403TENANT_MISMATCHJWT tenant ≠ X-Tenant-Id header
409IDEMPOTENCY_CONFLICTSame key + different body
413REQUEST_TOO_LARGEKong body size exceeded
429RATE_LIMITEDKong rate-limit-advanced (not reached this service)
503DEPENDENCY_UNAVAILABLERedis 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..."
}

See standards/ERROR_CODES.

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 → 409 IDEMPOTENCY_CONFLICT.
  • Missing key: allowed; no replay protection.

5. Pagination (GET /v1/sms — future)

Cursor-only: ?cursor=<opaque>&limit=<1..100>. Link: <...>; rel="next".