> ## Documentation Index
> Fetch the complete documentation index at: /llms.txt
> Use this file to discover all available pages before exploring further.

# Simiz Provider Protocol

> The strict opt-in exchange standard for connecting a provider to Simiz — payload, vocabularies, certification, one-paste adapter, and JSON Schema. Simiz orchestrates, the provider executes (non-custodial).

The **Simiz Provider Protocol** is the formal, **opt-in**, and **strict** exchange contract between a provider (Mobile Money operator, bank, aggregator) and Simiz. You implement this standard once, certify against the real pipeline, and become routable — without Simiz having to map your proprietary schema.

<Note>
  Simiz is **non-custodial orchestration**: the provider **executes** payments on its own licensed
  rails; Simiz **orchestrates** routing, monitoring, and reconciliation. Simiz does not hold funds
  and does not host your adapter.
</Note>

## Webhook or Pull API?

Choose the transport per rail. Both carry the same protocol payloads and the same `idempotency_key`; only delivery changes.

| Use case                                                                  | Choose       | Why                                                                                                        |
| ------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------- |
| You can expose a stable public HTTPS endpoint and verify HMAC signatures  | **Webhook**  | Lowest latency. Simiz pushes `health.check`, `payment.execute`, and `payout.execute` to your `baseUrl`.    |
| Your core system sits behind a firewall, VPN, NAT, or batch worker        | **Pull API** | No inbound exposure. Your worker polls `/v1/provider/orders` with a provider API key, executes, then acks. |
| You need strict network allowlisting or cannot receive internet callbacks | **Pull API** | The provider initiates every outbound connection to Simiz.                                                 |
| You need synchronous delivery visibility from Simiz ops                   | **Webhook**  | Simiz sees HTTP status, latency, retries, and bad acks immediately.                                        |
| You need simple migration from file/batch processing                      | **Pull API** | Polling can run on the same scheduler that already drives your settlement jobs.                            |

You can run one rail in Webhook mode and another in Pull mode. Do not implement both transports for the same rail unless an operator explicitly opens a migration window; transport changes are audited and the rail must be re-tested before it routes again.

## Health check (Simiz → you)

The protocol has two outbound order transports. With **Webhook**, Simiz sends signed calls to your execution endpoint (`baseUrl`) — the health probe below, the [`payment.execute` order](#payment-execution-simiz-you), and, when payouts are enabled, [`payout.execute`](#payout-execution-simiz-you). With **Pull API**, Simiz stores the same order payloads and your system fetches them from `/v1/provider/orders`.

For **Webhook** rails, when you (or a Simiz operator) click **Test rail** in the portal, Simiz sends a signed `POST` to the **same execution endpoint** (`baseUrl`) you declared in the portal, to prove it holds the rail webhook secret and that your rail answers. The rail is marked **TESTED** only when your handler replies **HTTP 200** with the matching nonce echoed back. Anything else — non-2xx, a wrong or missing nonce, an unreachable host, or a non-HTTPS endpoint — fails the probe, and the rail stays **PENDING** and cannot go live. **Pull API** rails do not declare `baseUrl`; their orders are fetched by API key.

**Request — Simiz → `POST {baseUrl}`**

| Header              | Value                                                           |
| ------------------- | --------------------------------------------------------------- |
| `Content-Type`      | `application/json`                                              |
| `X-Simiz-Signature` | `t={unix_ts},v1={hex_hmac}` — see [signature](#signature) below |
| `X-Simiz-Timestamp` | `{unix_ts}`                                                     |
| `X-Simiz-Event`     | `health.check`                                                  |

```json
{
  "protocol_version": "1",
  "type": "health.check",
  "nonce": "<hex>",
  "timestamp": 1752489600
}
```

**Your handler must**

1. **Verify the signature** exactly as for inbound events (same scheme, same rail webhook secret `whsec_…`; see [signature](#signature)). Reject with **401** or **403** if it does not match — this is how you confirm the call really comes from Simiz.
2. Respond **HTTP 200** with the nonce echoed back:

```json
{
  "type": "health.ack",
  "nonce": "<same nonce echoed back>"
}
```

<Note>
  Constraints: the endpoint **must be HTTPS**, the probe **times out at \~5 s**, and Simiz marks the
  rail **TESTED** only on **`200` + a `nonce` that matches** the one it sent. Echo the exact `nonce`
  you received — a wrong or missing nonce is treated as a failed probe.
</Note>

### Signature

The `X-Simiz-Signature` header uses HMAC-SHA256 for **Webhook transport**. Simiz signs (and you verify) the exact string `` `{timestamp}.{rawBody}` `` — the `X-Simiz-Timestamp` value, a literal dot, then the **raw request body** verbatim — with your rail's **webhook secret** (`whsec_…`), and encodes the digest as hex (the `v1=` part of the header). This is the `generateWebhookSignature` scheme; verify against the raw body **before** parsing JSON. Pull orders are authenticated by provider API key instead; settlement events you push back to Simiz use your provider webhook/channel signing secret.

## Payment execution (Simiz → you)

With **Webhook transport**, when the Simiz router selects your rail for a payment, Simiz sends a signed **`payment.execute`** order to the **same execution endpoint** (`baseUrl`), over the **same envelope** as the health check (same headers, same signature scheme, same shared secret — only `X-Simiz-Event` changes to `payment.execute`). With **Pull API**, fetch the same payload from `/v1/provider/orders`.

**Request — Simiz → `POST {baseUrl}`**

```json
{
  "protocol_version": "1",
  "type": "payment.execute",
  "idempotency_key": "pexec_9f2c...",
  "amount": "1500.00",
  "currency": "XAF",
  "network": "MTN_MOMO",
  "country": "CM",
  "payee": { "msisdn": "+237670000000" },
  "transaction_ref": "TX-2026-000123",
  "timestamp": 1752489600
}
```

### Field constraints

| Field              | Type / constraint                                                       |
| ------------------ | ----------------------------------------------------------------------- |
| `protocol_version` | literal `"1"`                                                           |
| `type`             | literal `"payment.execute"`                                             |
| `idempotency_key`  | non-empty string — **the correlation key** for this order (see below)   |
| `amount`           | **decimal string**, **major** units — same rule as `SettlementEvent`    |
| `currency`         | 3-letter ISO-4217 code                                                  |
| `network`          | same vocabulary as `SettlementEvent`                                    |
| `country`          | ISO-3166-1 alpha-2 (for example `CM`)                                   |
| `payee`            | **exactly one** of `{ "msisdn": "<E.164>" }` or `{ "ref": "<opaque>" }` |
| `transaction_ref`  | non-empty string — the Simiz transaction reference                      |
| `timestamp`        | Unix **seconds**, positive integer                                      |

**Your handler must**

1. **Verify the signature** first, exactly as for the health check (401/403 on mismatch).
2. Respond **HTTP 200** synchronously with a `payment.ack` **echoing the `idempotency_key`**:

```json
{
  "type": "payment.ack",
  "idempotency_key": "pexec_9f2c...",
  "status": "ACCEPTED"
}
```

To refuse the order (unsupported corridor, blocked payee, …), reply `"status": "REJECTED"` with an optional human-readable `"reason"`. Anything else — non-2xx, an unparseable body, a wrong or missing `idempotency_key` echo — is treated as a **failed delivery** and the payment is not considered accepted.

3. **Execute the payment on your licensed rail**, asynchronously.
4. **Push the outcome** as a regular inbound [`SettlementEvent`](#settlementevent-payload) to your Simiz webhook, reusing the **same `idempotency_key`** you received (or referencing the same `transaction_ref`). That settlement event — not the ack — is the truth of the outcome: it is what completes or fails the transaction on the Simiz side.

<Note>
  The ack means "**accepted for execution**", never "paid". The protocol has **no status-poll
  endpoint**: the settlement event you push is the single source of truth for the outcome, and the
  `idempotency_key` is the thread that ties order, ack, and settlement together. Deliveries may be
  retried with the **same** `idempotency_key` — treat a key you have already seen as a duplicate and
  do **not** execute twice. Simiz retries transient delivery failures (timeout, 502, 503, 504) up to
  3 attempts with the same `idempotency_key`.
</Note>

## Pull transport (you → Simiz)

If you cannot expose a public HTTPS endpoint, choose **Pull API** as the rail's **Order transport** in the portal. The message is still the same `payment.execute` or `payout.execute` payload, but your system fetches it from Simiz with a provider API key instead of receiving a signed webhook POST.

Use a provider API key with `Authorization: Bearer sk_...`:

| Method | Route                                         | Purpose                                                                                                                              |
| ------ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `GET`  | `/v1/provider/orders?status=pending&limit=50` | Fetch pending orders for the authenticated provider. First delivery is marked `DELIVERED`; orders remain re-deliverable until acked. |
| `POST` | `/v1/provider/orders/:idempotencyKey/ack`     | Ack with the exact `payment.ack` or `payout.ack` body. Re-sending the same ack is idempotent.                                        |
| `GET`  | `/v1/provider/orders/:idempotencyKey`         | Read one order's status for support/debugging.                                                                                       |

```bash
curl -H "Authorization: Bearer $SIMIZ_PROVIDER_API_KEY" \
  "https://api.simiz.io/v1/provider/orders?status=pending&limit=10"
```

**Official minimal worker loop — Node**

```js
const SIMIZ_API_URL = process.env.SIMIZ_API_URL ?? 'https://api.simiz.io';
const SIMIZ_PROVIDER_API_KEY = process.env.SIMIZ_PROVIDER_API_KEY;

async function pullOnce() {
  const res = await fetch(`${SIMIZ_API_URL}/v1/provider/orders?status=pending&limit=50`, {
    headers: { authorization: `Bearer ${SIMIZ_PROVIDER_API_KEY}` },
  });
  if (!res.ok) throw new Error(`pull failed: ${res.status} ${await res.text()}`);

  const { orders } = await res.json();
  for (const order of orders) {
    const payload = order.payload;
    const idempotencyKey = order.idempotency_key;
    const accepted =
      payload.type === 'payment.execute'
        ? await executePaymentIdempotently(payload)
        : payload.type === 'payout.execute'
          ? await executePayoutIdempotently(payload)
          : false;

    const ack = {
      type: payload.type === 'payout.execute' ? 'payout.ack' : 'payment.ack',
      idempotency_key: idempotencyKey,
      status: accepted ? 'ACCEPTED' : 'REJECTED',
      ...(!accepted ? { reason: 'Unsupported or duplicate order' } : {}),
    };

    const ackRes = await fetch(`${SIMIZ_API_URL}/v1/provider/orders/${idempotencyKey}/ack`, {
      method: 'POST',
      headers: {
        authorization: `Bearer ${SIMIZ_PROVIDER_API_KEY}`,
        'content-type': 'application/json',
      },
      body: JSON.stringify(ack),
    });
    if (!ackRes.ok) throw new Error(`ack failed ${idempotencyKey}: ${ackRes.status}`);
  }
}

setInterval(() => pullOnce().catch((err) => console.error(err)), 5000);
```

Pull transport authenticates the provider with the API key and TLS. Webhook transport authenticates each pushed order with `X-Simiz-Signature`. Both transports deliver the same v1 payload and use the same `idempotency_key` correlation. If an order is never pulled or acked before `expires_at`, Simiz expires it and fails the correlated transaction or payout.

## Payout execution (Simiz → you)

When a rail is marked **Supports payouts** in the provider portal, Simiz can dispatch **`payout.execute`** orders through the rail's selected transport: signed POST to `baseUrl` for Webhook, or `/v1/provider/orders` for Pull API. This is the outbound money movement contract for salaries, marketplace seller payouts, agent commissions, and refunds that leave a merchant balance.

**Request — Simiz → `POST {baseUrl}`**

```json
{
  "protocol_version": "1",
  "type": "payout.execute",
  "idempotency_key": "pexo_9f2c...",
  "amount": "25000.00",
  "currency": "XAF",
  "network": "MTN_MOMO",
  "country": "CM",
  "payee": { "msisdn": "+237670000000" },
  "transaction_ref": "PO-2026-000123",
  "timestamp": 1752489600
}
```

The fields follow the same constraints as `payment.execute`; only `type`, the `pexo_` idempotency prefix, and the business direction change. Reply synchronously with:

```json
{
  "type": "payout.ack",
  "idempotency_key": "pexo_9f2c...",
  "status": "ACCEPTED"
}
```

To refuse the payout, return `"status": "REJECTED"` with a `"reason"`. Simiz never dispatches a payout before its own balance, idempotency, environment, and `supportsPayout` gates pass; your ack only confirms that your rail accepted the order for asynchronous execution. The final truth still comes back as a `SettlementEvent` using the same `idempotency_key`.

## Routing

<Frame caption="The dispatcher routes the event by the presence of the protocol_version field: the payload is strictly validated (200 ACK on success, field-by-field 422 on violation); a payload without the field follows your existing proprietary adapter, unchanged. Simiz orchestrates, the provider executes (non-custodial).">
  <img src="/images/provider-protocol-dispatch.svg" alt="Dispatch diagram: a provider SettlementEvent reaches the Simiz dispatcher; if protocol_version is present, strict schema validation (200 ACK or field-by-field 422); otherwise the existing proprietary adapter." />
</Frame>

The dispatcher routes based on the **presence of the `protocol_version` field** in the event body:

* payload **with** `protocol_version: "1"` → **strict** schema validation, **field-by-field 422** rejection on any violation;
* payload **without** `protocol_version` → your **existing proprietary adapter** (mapping unchanged).

No silent switch: adopting the protocol is an explicit choice carried by the payload itself.

## `SettlementEvent` payload

The v1 settlement event pushed to your webhook carries exactly these fields:

```json
{
  "protocol_version": "1",
  "idempotency_key": "evt_9f2c...",
  "transaction_ref": "TX-2026-000123",
  "amount": "1500.00",
  "currency": "XAF",
  "status": "COMPLETED",
  "network": "ORANGE_MONEY",
  "settled_at": "2026-07-13T10:00:00Z"
}
```

### Field constraints

| Field              | Type / constraint                                                           |
| ------------------ | --------------------------------------------------------------------------- |
| `protocol_version` | literal `"1"`                                                               |
| `idempotency_key`  | non-empty string — stable idempotency key per settlement                    |
| `transaction_ref`  | non-empty string — your transaction reference                               |
| `amount`           | **decimal string**, **major** units (for example `"1500.00"`) — never cents |
| `currency`         | 3-letter ISO-4217 code (for example `XAF`)                                  |
| `status`           | see vocabulary below                                                        |
| `network`          | see vocabulary below                                                        |
| `settled_at`       | ISO-8601 datetime with offset (for example `2026-07-13T10:00:00Z`)          |

### `status` vocabulary

| Value       | Meaning                                 |
| ----------- | --------------------------------------- |
| `PENDING`   | settlement initiated, not yet confirmed |
| `COMPLETED` | settlement confirmed                    |
| `FAILED`    | settlement failed                       |
| `CANCELLED` | settlement cancelled                    |
| `REFUNDED`  | settlement refunded                     |

### `network` vocabulary

| Value          | Network          |
| -------------- | ---------------- |
| `ORANGE_MONEY` | Orange Money     |
| `MTN_MOMO`     | MTN Mobile Money |
| `WAVE`         | Wave             |
| `MOOV_MONEY`   | Moov Money       |

The body is signed with HMAC-SHA256 (`x-simiz-signature` header). Any violation returns a **422** listing every invalid field — never a silent failure.

## JSON Schema

The JSON Schema for `SettlementEvent` v1 is the **source of truth** against which Simiz validates, from which the one-paste adapter is generated, and which you can give to your own validator or code generator.

From the authenticated provider portal, retrieve it as-is:

```
GET /v1/provider/agent/assist/protocol-v1-schema
```

The response is the complete JSON Schema for the 8 fields above (types, enumerations, constraints). It is the same artifact used by the certification pipeline and by the generated adapter.

## Certification

From the portal (**Integration → Compliance → Certify now**), Simiz runs **12 canonical cases** through the **real pipeline** — 7 inbound cases on your settlement events, then 5 outbound cases proving the `payment.execute` contract against a reference handler. If a TESTED or ACTIVE rail has **Supports payouts** enabled, Simiz adds 5 payout outbound cases, so payout-capable certification is **17 cases**:

<Steps titleSize="h3">
  <Step title="Success" stepNumber="1">
    A valid `COMPLETED` `SettlementEvent` is accepted end to end.
  </Step>

  <Step title="Failure" stepNumber="2">
    A `FAILED` settlement is processed without incorrectly mutating the transaction.
  </Step>

  <Step title="Decimal amount" stepNumber="3">
    An `amount` in major units (for example `"1500.00"`) is accepted; cents are rejected.
  </Step>

  <Step title="Idempotent duplicate" stepNumber="4">
    The same returned `idempotency_key` is detected as duplicate (`409`) and ignored.
  </Step>

  <Step title="Status outside vocabulary" stepNumber="5">
    An unknown `status` is rejected with **422** and the invalid field.
  </Step>

  <Step title="Non-ISO date" stepNumber="6">
    A malformed `settled_at` is rejected with **422**.
  </Step>

  <Step title="Invalid signature" stepNumber="7">
    A body whose HMAC signature does not match is rejected (`403`).
  </Step>

  <Step title="Outbound — order acked" stepNumber="8">
    A signed `payment.execute` is accepted (`payment.ack` `ACCEPTED` with the `idempotency_key`
    echoed).
  </Step>

  <Step title="Outbound — foreign key echo" stepNumber="9">
    An ack echoing a **different** `idempotency_key` is treated as a failed delivery — never a false
    success.
  </Step>

  <Step title="Outbound — non-2xx" stepNumber="10">
    An endpoint answering HTTP 500 fails the delivery explicitly.
  </Step>

  <Step title="Outbound — timeout" stepNumber="11">
    A silent endpoint fails with an explicit `TIMEOUT`.
  </Step>

  <Step title="Outbound — signature refused" stepNumber="12">
    A handler holding the wrong secret rejects the signature (401) — proving your handler actually
    verifies.
  </Step>
</Steps>

When all cases are green, your channel carries the **CERTIFIED** badge. Each red case is diagnosed clearly, with the exact fix to apply.

<Note>
  Payout-capable certification adds the same five delivery checks for `payout.execute`: acked,
  foreign `idempotency_key`, non-2xx response, timeout, and signature refused. Collection-only
  providers remain certifiable with the original 12 cases.
</Note>

## One-paste adapter

The portal generates a **Node** or **Python** adapter (\~30 lines) that maps your raw data to the v1 payload, signs the body (sha256 hex), and posts it to your Simiz webhook. You **deploy it on your side** — Simiz does not host, execute, or store anything.

```js
// Generated by Simiz — Protocol v1. Adapt to YOUR data, then deploy ON YOUR SIDE (Simiz hosts nothing).
import crypto from 'node:crypto';

const SIMIZ_SIGNING_SECRET = process.env.SIMIZ_SIGNING_SECRET; // your Simiz secret
const SIMIZ_WEBHOOK_URL = process.env.SIMIZ_WEBHOOK_URL; // your Simiz webhook endpoint

// settlement = your raw settlement record.
export async function sendSettlementToSimiz(settlement) {
  // TODO: map your fields → Protocol v1 payload (amounts in MAJOR units, e.g. "1500.00").
  const payload = {
    protocol_version: '1',
    idempotency_key: settlement.id, // TODO: unique/stable key per settlement
    transaction_ref: settlement.transactionRef, // TODO: your transaction reference
    amount: String(settlement.amount), // TODO: decimal string in major units
    currency: settlement.currency, // TODO: 3-letter ISO-4217 code (e.g. "XAF")
    status: settlement.status, // TODO: PENDING|COMPLETED|FAILED|CANCELLED|REFUNDED
    network: settlement.network, // TODO: ORANGE_MONEY|MTN_MOMO|WAVE|MOOV_MONEY
    settled_at: settlement.settledAt, // TODO: ISO-8601 with offset (e.g. "2026-07-13T10:00:00Z")
  };

  // Sign the EXACT string sent (sha256 hex) — do not serialize again elsewhere.
  const body = JSON.stringify(payload);
  const signature = crypto.createHmac('sha256', SIMIZ_SIGNING_SECRET).update(body).digest('hex');

  const res = await fetch(SIMIZ_WEBHOOK_URL, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-simiz-signature': signature,
    },
    body,
  });
  if (!res.ok) throw new Error(`Simiz webhook rejected: ${res.status} ${await res.text()}`);
  return res;
}
```

<Note>
  The portal can also **pre-fill** this adapter from a real sample: paste a CSV export or JSON
  record, Simiz infers which of your fields correspond to the 8 protocol fields and returns the
  pre-mapped adapter — unresolved fields remain marked `// TODO` (no silent guessing).
</Note>

The adapter above **sends** events to Simiz. On the **receiving** side — the `baseUrl` handler that accepts Simiz's inbound calls — verify the signature against the raw body, then answer the [health check](#health-check-simiz-you) and the [payment order](#payment-execution-simiz-you):

```js
// baseUrl handler — verify X-Simiz-Signature over `${timestamp}.${rawBody}` FIRST (401/403 if invalid).
const event = JSON.parse(rawBody);

if (event.type === 'health.check') {
  return { type: 'health.ack', nonce: event.nonce }; // echo the nonce, respond HTTP 200
}

if (event.type === 'payment.execute') {
  // Duplicate idempotency_key? Ack it again, do NOT execute twice.
  enqueueExecutionOnYourRail(event); // async — your licensed rail does the money movement
  // Later: push a SettlementEvent to your Simiz webhook with this same idempotency_key.
  return { type: 'payment.ack', idempotency_key: event.idempotency_key, status: 'ACCEPTED' };
}

if (event.type === 'payout.execute') {
  enqueuePayoutOnYourRail(event); // async — payout-capable rails only
  return { type: 'payout.ack', idempotency_key: event.idempotency_key, status: 'ACCEPTED' };
}

// ... handle other v1 events here
```

## For AI assistants (Copilot, Claude)

Give this page as-is to your coding copilot, or open it in an assistant with a pre-filled prompt:

<CardGroup cols={2}>
  <Card title="Open in ChatGPT" icon="robot" href="https://chatgpt.com/?q=Implement%20a%20provider%20endpoint%20for%20the%20Simiz%20Provider%20Protocol.%20Full%20spec%20(signed%20payloads%2C%20SettlementEvent%2C%20HMAC-SHA256%2C%20certification)%3A%20https%3A%2F%2Fdocs.simiz.io%2Fen%2Fproviders%2Fsimiz-protocol%20%E2%80%94%20generate%20the%20HTTPS%20baseUrl%20handler%20and%20the%20signed%20SettlementEvent%20emission%20on%20the%20provider%20side." />

  <Card title="Open in Claude" icon="sparkles" href="https://claude.ai/new?q=Implement%20a%20provider%20endpoint%20for%20the%20Simiz%20Provider%20Protocol.%20Full%20spec%20(signed%20payloads%2C%20SettlementEvent%2C%20HMAC-SHA256%2C%20certification)%3A%20https%3A%2F%2Fdocs.simiz.io%2Fen%2Fproviders%2Fsimiz-protocol%20%E2%80%94%20generate%20the%20HTTPS%20baseUrl%20handler%20and%20the%20signed%20SettlementEvent%20emission%20on%20the%20provider%20side." />
</CardGroup>

Or copy the full prompt pack:

```text
You are implementing a provider endpoint for Simiz Provider Protocol v1.

Goal:
- Implement one HTTPS `baseUrl` handler on the provider side for signed Simiz -> provider calls.
- Emit signed provider -> Simiz `SettlementEvent` payloads from your rail after execution.
- Simiz orchestrates only; the provider executes payments on its licensed rails and hosts this code.

Direction 1: Simiz -> provider `baseUrl`
- Verify `X-Simiz-Signature` before JSON parsing.
- Signature scheme: HMAC-SHA256 hex over `{X-Simiz-Timestamp}.{rawBody}` using the same rail webhook secret `whsec_...`.
- `X-Simiz-Event: health.check` payload:
  { "protocol_version": "1", "type": "health.check", "nonce": "<hex>", "timestamp": 1752489600 }
- Reply HTTP 200:
  { "type": "health.ack", "nonce": "<same nonce echoed back>" }
- `X-Simiz-Event: payment.execute` payload schema:
  {
    "protocol_version": "1",
    "type": "payment.execute",
    "idempotency_key": "pexec_9f2c...",
    "amount": "1500.00",
    "currency": "XAF",
    "network": "MTN_MOMO",
    "country": "CM",
    "payee": { "msisdn": "+237670000000" },
    "transaction_ref": "TX-2026-000123",
    "timestamp": 1752489600
  }
- `payee` must contain exactly one of `msisdn` or `ref`.
- Reply HTTP 200 synchronously:
  { "type": "payment.ack", "idempotency_key": "<same key>", "status": "ACCEPTED" }
- To refuse, use `"status": "REJECTED"` and optional `"reason"`.
- The ack only means accepted for async execution, not paid.
- Deduplicate by `idempotency_key`; retries use the same key. Do not execute twice.
- Later, push a `SettlementEvent` to Simiz with the same `idempotency_key` or matching `transaction_ref`.
- Optional payout rail support uses the same envelope with `X-Simiz-Event: payout.execute`:
  {
    "protocol_version": "1",
    "type": "payout.execute",
    "idempotency_key": "pexo_9f2c...",
    "amount": "25000.00",
    "currency": "XAF",
    "network": "MTN_MOMO",
    "country": "CM",
    "payee": { "msisdn": "+237670000000" },
    "transaction_ref": "PO-2026-000123",
    "timestamp": 1752489600
  }
- Reply HTTP 200:
  { "type": "payout.ack", "idempotency_key": "<same key>", "status": "ACCEPTED" }
- Reject with `"status": "REJECTED"` and optional `"reason"` when the payout cannot be accepted.
- If using Pull API instead of a webhook `baseUrl`, poll `/v1/provider/orders`, execute each returned payload, then POST the matching `payment.ack` or `payout.ack` to `/v1/provider/orders/:idempotencyKey/ack`.

Direction 2: provider -> Simiz `SettlementEvent`
- Send exactly these 8 fields:
  {
    "protocol_version": "1",
    "idempotency_key": "evt_9f2c...",
    "transaction_ref": "TX-2026-000123",
    "amount": "1500.00",
    "currency": "XAF",
    "status": "COMPLETED",
    "network": "ORANGE_MONEY",
    "settled_at": "2026-07-13T10:00:00Z"
  }
- Field rules:
  - `protocol_version`: literal `"1"`
  - `idempotency_key`: non-empty stable idempotency key
  - `transaction_ref`: non-empty provider transaction reference
  - `amount`: decimal string in major units, never cents
  - `currency`: 3-letter ISO-4217 code
  - `status`: PENDING, COMPLETED, FAILED, CANCELLED, or REFUNDED
  - `network`: ORANGE_MONEY, MTN_MOMO, WAVE, or MOOV_MONEY
  - `settled_at`: ISO-8601 datetime with offset
- Sign the exact JSON body bytes you send. Do not re-serialize after signing.
- Post with `content-type: application/json` and `x-simiz-signature`.

Implementation guardrails:
- Use the raw body for signature verification.
- Use the rail `whsec_...` key for Webhook transport calls (`health.check`, `payment.execute`, `payout.execute`). Use your provider webhook/channel signing secret for settlement events sent back to Simiz.
- Reject invalid signatures with 401 or 403.
- Echo nonce and idempotency keys exactly.
- Preserve idempotency across order, ack, execution, and settlement.
- Return explicit errors for unsupported corridors or blocked payees; never silently accept.
```

Simiz remains **non-custodial**: the adapter runs on your side, on your rails; Simiz orchestrates.
