# Client API Integration

The authoritative public contract is [API_V1_CONTRACT.md](api/API_V1_CONTRACT.md) and [OpenAPI 3.1](api/openapi.yaml). If this guide differs, OpenAPI governs.

## Connection and authentication

Use HTTPS and API version prefix `/api/v1`. Send JSON with `Content-Type: application/json`, accept JSON, and authenticate every request with `Authorization: Bearer <API_TOKEN>`. Credentials are tenant-scoped; never share them between environments or log them.

Send `X-Correlation-ID` when tracing is required. It must be 1–100 allowed characters. The response header identifies the current HTTP request. For single-message idempotent replay, the body `correlation_id` remains the original submission correlation ID.

Submission endpoints require `Idempotency-Key`. Reuse a key only for the same tenant and canonically equivalent payload. Equivalent replay returns the original result; changed input returns `409 IDEMPOTENCY_CONFLICT`. Use a fresh unpredictable key for each intended operation.

## Errors

Request-level errors use:

```json
{"error":{"code":"INVALID_REQUEST","message":"The request is invalid.","details":{},"correlation_id":"<CORRELATION_ID>"}}
```

Branch on the stable `error.code`, not text. Respect `429` and `Retry-After`. Do not retry validation, authentication, authorization, not-found, or idempotency-conflict failures without correcting the cause.

## Addresses and sender handling

`recipient` is E.164: `+`, then 8–15 digits with a non-zero first digit. `sender_id` is optional, control-character-free, and at most 191 characters; omission succeeds only when the authenticated tenant/application has an approved default. Confirm permitted sender values with the operator.

## Send one message

`POST /api/v1/messages` accepts only `recipient`, `message`, optional `sender_id`, and optional `client_reference`. It returns HTTP `202` with `message_id`, public `status`, `created_at`, `correlation_id`, and `client_reference` when supplied. Persist `message_id`, your idempotency key, and the original result.

## Send a bulk request

`POST /api/v1/messages/bulk` accepts optional batch `client_reference` and `messages` containing 1–100 single-message inputs. It is ordered, best-effort processing: each result has its input `index`; valid siblings can be accepted when another item is rejected. Persist `batch_id` and every accepted `message_id`. The whole ordered body has one tenant-scoped idempotency key.

## Message status

The two retrieval endpoints are distinct:

- `GET /api/v1/messages/{message_id}` returns the full approved public resource: required `message_id`, `recipient`, `status`, `created_at`, and current-request `correlation_id`; optional `sender_id`, `client_reference`, `submitted_at`, and `finalized_at` appear only when applicable.
- `GET /api/v1/messages/{message_id}/status` returns the compact status resource: required `message_id`, normalized `status`, authoritative `occurred_at`, and current-request `correlation_id`; optional `client_reference` appears only when available.

Neither endpoint is an alias for the other. Requests are tenant-isolated. Public statuses are `queued`, `submitted`, `delivered`, `failed`, `expired`, `rejected`, and `unknown`.

Treat terminal statuses (`delivered`, `failed`, `expired`, `rejected`) as final. Reconcile non-terminal records periodically through status reads even when webhooks are enabled.

## Webhook configuration

`PUT /api/v1/webhook` creates or completely replaces the tenant's single configuration with `url`, `secret`, and `enabled`. Production URLs require HTTPS and the secret requires at least 32 cryptographically random bytes. `GET /api/v1/webhook` returns only its safe representation; the secret is write-only. A replaced secret becomes active immediately and the previous secret remains valid for receiver verification for 24 hours.

## Verify webhook deliveries

Read the raw request bytes before parsing JSON. Read `X-Webhook-Timestamp` and `X-Webhook-Signature`, reject timestamps outside five minutes, calculate lowercase hex HMAC-SHA256 over `<timestamp>.<raw-body>`, and compare `v1=<digest>` using a timing-safe function. Apply replay protection to a stable digest of timestamp/signature/body, then parse JSON and return 2xx promptly.

Webhook bodies contain only `message_id`, terminal `status`, optional `client_reference`, optional `delivered_at`, and `timestamp`. Processing must be idempotent because networks can redeliver even though the current gateway creates at most one delivery attempt per event.

## Production practices

- Separate test and production credentials, URLs, idempotency keys, and webhook secrets.
- Use bounded connect/read timeouts and safe retries only for transient failures.
- Never log tokens, secrets, message content, signatures, or full request bodies.
- Preserve request correlation headers and original submission identifiers.
- Store message IDs before initiating downstream work.
- Alert on sustained authentication, rate-limit, service-unavailable, webhook-verification, or reconciliation failures.

See [CLIENT_QUICK_START.md](CLIENT_QUICK_START.md) and the versioned [examples](../examples/README.md).
