# MHI Gateway Public API

## Milestone 2B contract

The implemented public surface follows OpenAPI 3.1 terminology and is versioned under `/api/v1`. Milestone 2B supports one transactional SMS message at a time. Dispatch, provider routing, billing, bulk messaging, OTP and webhooks are not part of this contract.

### Authentication and tracing

All message routes require a server API key:

```http
Authorization: Bearer mhi_<environment>_<prefix>.<secret>
```

The credential determines the tenant and application. `X-Tenant-ID` is ignored and ownership fields in payloads are rejected.

`X-Correlation-ID` is an optional request-tracing identifier containing 1–100 ASCII letters, digits, `.`, `_`, `:`, or `-`. The server generates one when absent. An invalid value returns `422` without echoing it. Every response has `X-Request-ID`, `X-Correlation-ID`, `meta.request_id`, and `meta.request_correlation_id`.

Request tracing is separate from the optional payload `correlation_id`, exposed as `message_correlation_id`.

### Submit an SMS

`POST /api/v1/messages`

```json
{
  "channel": "sms",
  "type": "transactional",
  "to": "+255712345678",
  "from": "MICROHEALTH",
  "message": "Example notification",
  "priority": "normal",
  "request_delivery_receipt": false,
  "correlation_id": "hospital-request-123",
  "metadata": {"source_reference": "ref-123"}
}
```

The optional `Idempotency-Key` header is trimmed, must contain visible ASCII characters, and may not exceed 191 characters. A successful request creates the message, SMS extension, acceptance event, and unpublished transactional outbox record atomically. It does not send, publish, or queue anything.

Response: `202 Accepted`

```json
{
  "data": {
    "id": "01J8X0Z7M7K7X8Y5A2D8A9C3F1",
    "channel": "sms",
    "message_status": "accepted",
    "delivery_status": "pending",
    "billing_status": "pending",
    "priority": "normal",
    "message_correlation_id": "hospital-request-123",
    "accepted_at": "2026-07-15T10:00:00+00:00"
  },
  "meta": {
    "request_id": "01K...",
    "request_correlation_id": "client-trace-123"
  }
}
```

An already-used non-null key in the same tenant/application returns deterministic `409 idempotency_conflict`. It does not reveal the existing message. Exact response replay requires a future request fingerprint and `api_idempotency_keys` record. The same key may be used by another application; an absent key permits separate messages.

### Retrieve an SMS

`GET /api/v1/messages/{message_public_id}`

Only the authenticated tenant and application are searched. Missing and out-of-scope identifiers both return `404`.

```json
{
  "data": {
    "id": "01J8X0Z7M7K7X8Y5A2D8A9C3F1",
    "channel": "sms",
    "type": "transactional",
    "message_status": "accepted",
    "delivery_status": "pending",
    "billing_status": "pending",
    "priority": "normal",
    "message_correlation_id": "hospital-request-123",
    "accepted_at": "2026-07-15T10:00:00+00:00",
    "created_at": "2026-07-15T10:00:00+00:00",
    "updated_at": "2026-07-15T10:00:00+00:00",
    "sms": {"recipient": "+********5678", "sender": "MICROHEALTH"}
  },
  "meta": {"request_id": "01K...", "request_correlation_id": "01K..."}
}
```

### List SMS messages

`GET /api/v1/messages`

Cursor pagination defaults to 25 and is bounded at 100. Ordering uses the stable pair `created_at, id`; only `sort=created_at` and `direction=asc|desc` are accepted. Default direction is newest first (`desc`).

Allowed filters are `message_status`, `delivery_status`, `billing_status`, `channel`, `type`, `priority`, `created_from`, `created_to`, and `correlation_id`. Recipient/body filtering and arbitrary column selection are prohibited. Pagination returns `links.next`, `links.previous`, `meta.next_cursor`, and `meta.previous_cursor`.

### Errors and status codes

```json
{
  "error": {
    "code": "validation_failed",
    "message": "The request payload is invalid.",
    "details": {}
  },
  "meta": {
    "request_id": "01K...",
    "request_correlation_id": "01K..."
  }
}
```

Implemented statuses are `400` invalid syntax, `401` authentication failure, `403` forbidden, `404` not found, `409` idempotency conflict, `422` validation failure, `429` rate limited, and sanitized `500` internal error. Rate-limited responses retain Laravel rate-limit headers and `Retry-After`.

### Privacy exclusions

Public resources never return tenant/application/credential database IDs, API keys, idempotency keys, full recipients, message bodies, recipient hashes, ciphertext, providers, provider references, raw metadata, outbox data, or database IDs. Validation and internal errors do not echo sensitive submitted values.
