# RC1 Release Candidate Handover

## Status and authority

RC1 implementation and internal verification are complete through RC1-6. Staging acceptance, live YAS acceptance, and production approval remain pending until evidence is recorded in the [acceptance checklist](ACCEPTANCE_CHECKLIST.md).

The public contract is defined by [API_V1_CONTRACT.md](api/API_V1_CONTRACT.md) and the authoritative [OpenAPI 3.1 document](api/openapi.yaml). If this guide conflicts with OpenAPI, OpenAPI governs.

| Milestone | Implemented scope |
| --- | --- |
| RC1-1 | API v1 contract and lifecycle freeze |
| RC1-1A | Ordered best-effort bulk submission contract |
| RC1-2 | Single-message validation, normalized responses, request correlation, and durable tenant-scoped idempotency |
| RC1-3 | Bulk submission with stable ordering, partial success, and durable tenant-scoped replay |
| RC1-4 | Tenant/application-scoped public message status retrieval |
| RC1-5 | Provider-message correlation, normalized immutable DLR evidence, and public status projection |
| RC1-5A | Tenant webhook configuration, security, event, timeout, and retention contract |
| RC1-6 | One tenant webhook configuration and one post-commit terminal webhook attempt |

RC1 does not add or redesign `RuntimeLoop`, `SessionExecutor`, SMPP reconnect behavior, a retry engine, billing, wallet behavior, campaigns, templates, scheduling, dashboards, or reporting. Milestone 6A remains deferred, not cancelled.

## Production-ready application flow

```text
single or bulk API submission
    -> durable tenant-scoped idempotency
    -> queued message persistence
    -> existing SMPP outbound execution
    -> provider acknowledgement and provider message identifier
    -> inbound DLR processing
    -> normalized immutable delivery evidence
    -> public status projection
    -> optional terminal tenant webhook
```

Webhook publication is optional because a tenant may have no configuration or may disable it. A terminal event is recorded atomically with an applied terminal DLR, and network publication occurs only after the outermost database commit. RC1 makes one attempt and does not retry.

## Public API integration guide

All endpoints use UTF-8 JSON and require `Authorization: Bearer <API_CREDENTIAL>`. Credentials resolve exactly one tenant and application. Cross-tenant or cross-application resources are not disclosed. Every response has `X-Correlation-ID`; a valid incoming value is preserved, otherwise the gateway generates a UUIDv4.

Errors use the standard envelope:

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

### POST /api/v1/messages

Purpose: submit one SMS message. Required headers are `Authorization`, `Idempotency-Key`, and `Content-Type: application/json`; `X-Correlation-ID` is optional.

```http
POST /api/v1/messages
Authorization: Bearer <API_CREDENTIAL>
Idempotency-Key: order-1042-message-1
X-Correlation-ID: request-1042
Content-Type: application/json

{
  "recipient": "+255712345678",
  "message": "Your verification code is 123456.",
  "sender_id": "MHI",
  "client_reference": "order-1042"
}
```

```json
{
  "message_id": "01JEXAMPLE0000000000000000",
  "status": "queued",
  "client_reference": "order-1042",
  "created_at": "2026-07-19T12:30:45.123456Z",
  "correlation_id": "request-1042"
}
```

Success is HTTP `202`. Within one tenant, the same idempotency key and equivalent payload replay the original message response; a different payload returns HTTP `409` with `IDEMPOTENCY_CONFLICT`. The response header always identifies the current HTTP request. On replay, the body retains the original submission correlation ID.

### POST /api/v1/messages/bulk

Purpose: evaluate 1–100 messages independently while preserving input order. Required headers are the same as single submission. One top-level `Idempotency-Key` covers the complete ordered request.

```http
POST /api/v1/messages/bulk
Authorization: Bearer <API_CREDENTIAL>
Idempotency-Key: import-42
Content-Type: application/json

{
  "client_reference": "campaign-42",
  "messages": [
    {
      "recipient": "+255712345678",
      "message": "First message",
      "sender_id": "MHI",
      "client_reference": "row-1"
    },
    {
      "recipient": "invalid",
      "message": "Second message"
    }
  ]
}
```

```json
{
  "batch_id": "550e8400-e29b-41d4-a716-446655440001",
  "client_reference": "campaign-42",
  "accepted": 1,
  "rejected": 1,
  "results": [
    {
      "index": 0,
      "message_id": "01JEXAMPLE0000000000000000",
      "status": "queued",
      "client_reference": "row-1"
    },
    {
      "index": 1,
      "error": {
        "code": "INVALID_RECIPIENT",
        "message": "Recipient is invalid.",
        "details": {}
      }
    }
  ]
}
```

Success is HTTP `202`, including an all-rejected valid batch. Equivalent replay preserves `batch_id`, counters, ordering, message identities, and item errors. A conflicting payload returns `409 IDEMPOTENCY_CONFLICT`. The correlation header represents the current request; there is no batch correlation field.

### GET /api/v1/messages/{message_id}

Purpose: retrieve the authenticated tenant/application's public message representation and projected status. Required header: `Authorization`; `X-Correlation-ID` is optional.

```http
GET /api/v1/messages/01JEXAMPLE0000000000000000
Authorization: Bearer <API_CREDENTIAL>
X-Correlation-ID: status-check-1
```

```json
{
  "message_id": "01JEXAMPLE0000000000000000",
  "recipient": "+255712345678",
  "sender_id": "MHI",
  "client_reference": "order-1042",
  "status": "delivered",
  "created_at": "2026-07-19T12:30:45.123456Z",
  "correlation_id": "status-check-1"
}
```

Optional sender and client-reference fields are omitted when unavailable. Absence and cross-scope access both return the standardized not-found behavior. This endpoint has no idempotency key. The frozen contract reserves optional `submitted_at` and `finalized_at`; the committed RC1-4 response does not currently emit them.

### PUT /api/v1/webhook

Purpose: create the tenant's single delivery-webhook configuration or replace it completely. Required headers are `Authorization` and `Content-Type`; `X-Correlation-ID` is optional.

```http
PUT /api/v1/webhook
Authorization: Bearer <API_CREDENTIAL>
Content-Type: application/json

{
  "enabled": true,
  "url": "https://receiver.example/webhooks/delivery",
  "secret": "<AT_LEAST_32_RANDOM_BYTES>"
}
```

```json
{
  "url": "https://receiver.example/webhooks/delivery",
  "enabled": true,
  "created_at": "2026-07-19T12:30:45.123456Z",
  "updated_at": "2026-07-19T12:30:45.123456Z"
}
```

Success is HTTP `200`. The secret is write-only. Replacement activates the new secret immediately and retains the prior secret's 24-hour overlap. There is no POST, DELETE, multiple-endpoint, priority, or event-filtering API. This endpoint has no idempotency key.

### GET /api/v1/webhook

Purpose: retrieve the safe representation of the authenticated tenant's one configuration.

```http
GET /api/v1/webhook
Authorization: Bearer <API_CREDENTIAL>
```

```json
{
  "url": "https://receiver.example/webhooks/delivery",
  "enabled": true,
  "created_at": "2026-07-19T12:30:45.123456Z",
  "updated_at": "2026-07-19T12:30:45.123456Z"
}
```

The response never includes the active secret, previous secret, ciphertext, or internal identifiers.

## Webhook receiver guide

Terminal webhook bodies contain only `message_id`, `status`, optional `client_reference`, optional `delivered_at`, and `timestamp`. Status is one of `delivered`, `failed`, `expired`, or `rejected`.

Each request includes:

- `X-Webhook-Timestamp`: Unix seconds;
- `X-Webhook-Signature`: `v1=<lowercase hexadecimal HMAC-SHA256>`.

The signing input is the exact byte sequence `<timestamp>.<raw-request-body>`. A receiver must use the raw body before JSON parsing or reserialization.

```text
timestamp = header("X-Webhook-Timestamp")
signature = header("X-Webhook-Signature")

if absolute_difference(current_unix_time, integer(timestamp)) > 300:
    reject

signed_bytes = timestamp + "." + raw_request_body
candidates = [active_secret]
if previous_secret_is_within_24_hour_overlap:
    candidates.append(previous_secret)

valid = any(
    timing_safe_equal(signature, "v1=" + hex(hmac_sha256(secret, signed_bytes)))
    for secret in candidates
)
if not valid:
    reject

process_idempotently()
return_2xx_promptly()
```

Never log either secret or signature material. Reject timestamps outside the five-minute replay window. Do not depend on retry behavior: RC1 performs exactly one delivery attempt.

## Readiness states

- Implementation complete: yes, through committed RC1-6.
- Internal automated verification complete: recorded by the milestone evidence; reproduce before approval.
- Staging acceptance: pending.
- YAS external acceptance: pending.
- Production approval: pending.

See [deployment](DEPLOYMENT.md), [operations](OPERATIONS.md), [YAS integration](YAS_INTEGRATION_GUIDE.md), and the [acceptance checklist](ACCEPTANCE_CHECKLIST.md).
