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

# Merchant Onboarding

> Sign up, get your API credentials, secure your requests, and receive webhooks on Faira.

Faira lets your customers pay with credit at checkout. This guide walks a merchant
from account creation to a fully wired integration: registering, authenticating
your API calls, initiating a checkout session, and receiving signed webhooks.

<Info>
  Everything below is real, working API behavior. Amounts are always in **minor
  units** (for example, `10000` = £100.00 / ₦100.00). Timestamps are **RFC 3339**
  unless a field is explicitly described as a Unix timestamp.
</Info>

## Base URLs

| Environment       | Base URL                       |
| ----------------- | ------------------------------ |
| Production        | `https://api.faira.io`         |
| Sandbox / Staging | `https://api.staging.faira.io` |
| Local development | `http://localhost:8080`        |

All routes in this guide are prefixed with `/api/v1`. Replace the host with the
environment you are integrating against.

<Note>
  Use the exact host issued to your account. If you are unsure which environment
  your credentials belong to, contact your Faira representative before going live.
</Note>

***

## Credentials & tokens at a glance

Faira issues several distinct secrets. They are **not** interchangeable — each is
used for a specific purpose.

| Credential                    | Prefix         | Where it is used                                                                                                              | Lifetime          |
| ----------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| **API key**                   | `fai_live_mk_` | Identifies your account. Sent as the `X-Api-Key` header (HMAC flow) or as a `Bearer` token for read-only dashboard endpoints. | Until rotated     |
| **API secret**                | `fai_secret_`  | Signs HMAC requests (`X-Signature`). **Shown once** at registration/rotation. Never sent over the wire.                       | Until rotated     |
| **Webhook signing secret**    | `fai_whsec_`   | Verifies webhooks Faira sends you, and signs payment events you send Faira.                                                   | Until rotated     |
| **Dashboard session token**   | `fai_mst_`     | Returned by login. Authorizes sensitive dashboard actions.                                                                    | 8 hours (default) |
| **Relationship access token** | `fai_at_`      | Short-lived token that authorizes a customer's limit/transaction calls.                                                       | \~15 minutes      |

<Warning>
  The **API secret** and **webhook signing secret** are shown only at creation or
  rotation and are never returned again by any endpoint. Store them in your secrets
  manager immediately. If lost, rotate your credentials to get a new pair.
</Warning>

***

## Step 1 — Create your merchant account

There are two ways an account is created. Most self-serve integrations use
registration; enterprise accounts are provisioned by the Faira team.

<Tabs>
  <Tab title="Self-service registration">
    Register directly and receive live credentials immediately. The account is created
    in `active` status — you can log in and integrate right away.

    ```http theme={null}
    POST /api/v1/merchants/register
    Content-Type: application/json
    ```

    <ParamField body="organization_name" type="string" required>
      Registered business / trading name. Must be unique across Faira.
    </ParamField>

    <ParamField body="contact_name" type="string" required>
      Primary contact's full name.
    </ParamField>

    <ParamField body="contact_email" type="string" required>
      Valid email. This becomes your dashboard login and receives the welcome email.
    </ParamField>

    <ParamField body="contact_phone" type="string" required>
      E.164 format, e.g. `+447700900000`.
    </ParamField>

    <ParamField body="business_type" type="string" required>
      One of `limited_company`, `sole_trader`, `partnership`, `other`.
    </ParamField>

    <ParamField body="country" type="string" required>
      ISO 3166-1 alpha-2 country code, e.g. `GB` or `NG`.
    </ParamField>

    <ParamField body="website_url" type="string">
      Optional business website (must be a valid URL).
    </ParamField>

    <ParamField body="webhook_url" type="string">
      Optional. HTTPS endpoint that will receive webhook deliveries. You can set this at
      registration or add it later via [your profile](#step-3-complete-your-profile).
      See [Webhook URL requirements](#webhook-url-requirements).
    </ParamField>

    <ParamField body="password" type="string" required>
      Minimum 8 characters. Used for dashboard login.
    </ParamField>

    <ParamField body="password_confirmation" type="string" required>
      Must match `password`.
    </ParamField>

    ```json Request theme={null}
    {
      "organization_name": "Acme Retail Ltd",
      "contact_name": "Jane Doe",
      "contact_email": "jane@acme.example",
      "contact_phone": "+447700900000",
      "business_type": "limited_company",
      "country": "GB",
      "website_url": "https://acme.example",
      "webhook_url": "https://acme.example/webhooks/faira",
      "password": "s3cure-passw0rd",
      "password_confirmation": "s3cure-passw0rd"
    }
    ```

    ```json 201 Created theme={null}
    {
      "merchant_id": "11111111-2222-3333-4444-555555555555",
      "api_key": "fai_live_mk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "api_secret": "fai_secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "status": "active",
      "organization_name": "Acme Retail Ltd",
      "business_type": "limited_company",
      "contact_name": "Jane Doe",
      "website_url": "https://acme.example",
      "webhook_url": "https://acme.example/webhooks/faira",
      "welcome_email_sent": true
    }
    ```

    <Warning>
      The `api_secret` in this response is the **only** time it is ever shown. Copy it
      now. The `webhook_signing_secret` is generated at the same time but is retrieved
      from your dashboard (see [Webhooks](#step-5-configure-webhooks)).
    </Warning>

    **Failure responses**

    | Status | Code                     | Meaning                                       |
    | ------ | ------------------------ | --------------------------------------------- |
    | `400`  | `invalid_json`           | Body is not valid JSON                        |
    | `409`  | `registration_failed`    | An organization with that name already exists |
    | `422`  | `passwords_do_not_match` | Password and confirmation differ              |
    | `422`  | `registration_failed`    | A required field failed validation            |
  </Tab>

  <Tab title="Admin-provisioned account">
    If your account is created for you by the Faira team, you receive a **set-password
    email** containing a one-time link. Complete it before your first login.

    ```http theme={null}
    POST /api/v1/merchants/set-password
    Content-Type: application/json
    ```

    ```json Request theme={null}
    {
      "merchant_id": "11111111-2222-3333-4444-555555555555",
      "token": "one-time-token-from-email",
      "password": "s3cure-passw0rd",
      "password_confirmation": "s3cure-passw0rd"
    }
    ```

    ```json 200 OK theme={null}
    { "status": "password_set" }
    ```

    <Note>
      The set-password link expires **48 hours** after your account is created. If it
      expires, use [Forgot password](#forgot--reset-password) to request a new link.
    </Note>
  </Tab>
</Tabs>

***

## Step 2 — Log in to the dashboard

Exchange your email and password for a **dashboard session token** (`fai_mst_`).
The session is required for sensitive actions such as rotating credentials.

```http theme={null}
POST /api/v1/merchants/login
Content-Type: application/json
```

```json Request theme={null}
{
  "email": "jane@acme.example",
  "password": "s3cure-passw0rd"
}
```

```json 200 OK theme={null}
{
  "session_token": "fai_mst_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "merchant_id": "11111111-2222-3333-4444-555555555555",
  "expires_at": "2026-07-13T20:00:00Z",
  "name": "Acme Retail Ltd",
  "api_key": "fai_live_mk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "webhook_url": null,
  "contact_name": "Jane Doe",
  "contact_email": "jane@acme.example",
  "business_type": "limited_company",
  "country": "GB",
  "website_url": "https://acme.example",
  "status": "active"
}
```

The session token expires after **8 hours** by default. Send it as a bearer token
on dashboard endpoints:

```http theme={null}
Authorization: Bearer fai_mst_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

<AccordionGroup>
  <Accordion title="Forgot / reset password">
    Request a reset link. The response is always `200` to prevent email enumeration —
    a link is only sent if the email exists.

    ```http theme={null}
    POST /api/v1/merchants/forgot-password
    ```

    ```json theme={null}
    { "email": "jane@acme.example" }
    ```

    Then apply the emailed token (valid for **1 hour**):

    ```http theme={null}
    POST /api/v1/merchants/reset-password
    ```

    ```json theme={null}
    {
      "token": "one-time-reset-token",
      "password": "new-s3cure-passw0rd",
      "password_confirmation": "new-s3cure-passw0rd"
    }
    ```
  </Accordion>
</AccordionGroup>

***

## Step 3 — Complete your profile

Fill in the fields Faira needs to route payments and settle funds to you. Send the
API key or a dashboard session on these endpoints.

```http theme={null}
GET   /api/v1/merchants/profile      # read your profile
PATCH /api/v1/merchants/profile      # update editable fields
```

```json PATCH body theme={null}
{
  "contact_email": "billing@acme.example",
  "contact_name": "Jane Doe",
  "contact_phone": "+447700900000",
  "website_url": "https://acme.example",
  "webhook_url": "https://acme.example/webhooks/faira",
  "settlement_account_no": "12345678",
  "settlement_bank_code": "040004"
}
```

```json 200 OK theme={null}
{ "status": "updated" }
```

<Note>
  `organization_name`, `business_type`, `country`, and `status` are **locked** after
  registration and cannot be changed here. Contact support if any of these need to
  change.
</Note>

Both endpoints accept **either** an API key or a dashboard session token:

```http theme={null}
Authorization: Bearer fai_live_mk_...     # API key
# or
Authorization: Bearer fai_mst_...         # dashboard session
```

***

## Step 4 — Store & rotate credentials

Rotating credentials **revokes your current API key and secret** and issues a new
pair. This requires an active **dashboard session** — API-key auth is intentionally
rejected here so a leaked key cannot rotate itself.

```http theme={null}
POST /api/v1/merchants/credentials/reset
Authorization: Bearer fai_mst_...
```

```json 200 OK theme={null}
{
  "api_key": "fai_live_mk_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
  "api_secret": "fai_secret_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
}
```

<Warning>
  The previous key and secret stop working immediately. Update your servers with the
  new pair **before** rotating in production to avoid downtime.
</Warning>

***

## Authenticating your requests

Faira supports three auth styles depending on the endpoint:

<CardGroup cols={3}>
  <Card title="API key (bearer)">
    Read-only dashboard endpoints. Send `Authorization: Bearer fai_live_mk_...`.
  </Card>

  <Card title="Dashboard session">
    Sensitive dashboard actions. Send `Authorization: Bearer fai_mst_...`.
  </Card>

  <Card title="HMAC signing">
    Server-to-server checkout endpoints. Sign the request with your API secret.
  </Card>
</CardGroup>

### HMAC request signing

Checkout endpoints (`POST /api/v1/sessions/initiate`, `POST /api/v1/sessions/token`)
require a signed request. Send these headers:

| Header          | Value                                                      |
| --------------- | ---------------------------------------------------------- |
| `X-Merchant-Id` | Your merchant UUID                                         |
| `X-Api-Key`     | Your `fai_live_mk_` API key                                |
| `X-Timestamp`   | Current time in **RFC 3339** (e.g. `2026-07-13T12:00:00Z`) |
| `X-Nonce`       | A unique, single-use string per request                    |
| `X-Signature`   | Lowercase hex HMAC-SHA256 (see below)                      |

The **string-to-sign** is five lines joined by `\n` (newline), in this exact order:

```text theme={null}
METHOD
/request/path
timestamp
nonce
raw-json-body
```

The signature is `HMAC-SHA256(api_secret, string-to-sign)`, hex-encoded and
**lowercase**. The `raw-json-body` must be the **exact bytes** you send — sign the
serialized body, then send that same body unchanged.

<Warning>
  Faira rejects a request if the `X-Timestamp` is more than **5 minutes** from server
  time, or if the `X-Nonce` has been seen before (nonces are single-use for \~6
  minutes). Generate a fresh timestamp and nonce for every request.
</Warning>

<CodeGroup>
  ```js Node.js theme={null}
  import crypto from "node:crypto";

  function signRequest({ apiSecret, method, path, body }) {
    const timestamp = new Date().toISOString().replace(/\.\d+Z$/, "Z"); // RFC3339
    const nonce = crypto.randomUUID();
    const rawBody = JSON.stringify(body);

    const stringToSign = [method.toUpperCase(), path, timestamp, nonce, rawBody].join("\n");
    const signature = crypto
      .createHmac("sha256", apiSecret)
      .update(stringToSign)
      .digest("hex"); // lowercase hex

    return { timestamp, nonce, rawBody, signature };
  }

  // usage
  const { timestamp, nonce, rawBody, signature } = signRequest({
    apiSecret: process.env.FAIRA_API_SECRET,
    method: "POST",
    path: "/api/v1/sessions/initiate",
    body: { merchant_id: "1111...", transaction: { amount: 10000, currency: "GBP" } },
  });

  await fetch("https://api.faira.io/api/v1/sessions/initiate", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Merchant-Id": "11111111-2222-3333-4444-555555555555",
      "X-Api-Key": process.env.FAIRA_API_KEY,
      "X-Timestamp": timestamp,
      "X-Nonce": nonce,
      "X-Signature": signature,
    },
    body: rawBody, // send the exact bytes that were signed
  });
  ```

  ```python Python theme={null}
  import hmac, hashlib, json, uuid
  from datetime import datetime, timezone

  def sign_request(api_secret: str, method: str, path: str, body: dict):
      timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
      nonce = str(uuid.uuid4())
      raw_body = json.dumps(body, separators=(",", ":"))

      string_to_sign = "\n".join([method.upper(), path, timestamp, nonce, raw_body])
      signature = hmac.new(
          api_secret.encode(), string_to_sign.encode(), hashlib.sha256
      ).hexdigest()  # lowercase hex

      return timestamp, nonce, raw_body, signature
  ```

  ```go Go theme={null}
  func signRequest(apiSecret, method, path, timestamp, nonce string, body []byte) string {
  	payload := strings.Join([]string{
  		strings.ToUpper(method),
  		path,
  		timestamp,
  		nonce,
  		string(body),
  	}, "\n")
  	mac := hmac.New(sha256.New, []byte(apiSecret))
  	mac.Write([]byte(payload))
  	return hex.EncodeToString(mac.Sum(nil))
  }
  ```
</CodeGroup>

***

## Checkout session flow

Once authenticated, you initiate a checkout session for a customer. This creates a
pending transaction and returns a redirect URL that takes your customer into
Faira's hosted login/onboarding flow.

<Steps>
  <Step title="Initiate the session">
    `POST /api/v1/sessions/initiate` (HMAC-signed). Include the transaction and,
    for returning customers, their `customer_token`.

    ```json Request theme={null}
    {
      "merchant_id": "11111111-2222-3333-4444-555555555555",
      "unique_customer_reference": "cust_7890",
      "redirect_url": "https://acme.example/faira/callback",
      "transaction": {
        "amount": 10000,
        "currency": "GBP",
        "description": "Order #1234",
        "merchant_ref": "order_1234",
        "metadata": { "cart_id": "cart_456" }
      }
    }
    ```

    ```json 201 Created theme={null}
    {
      "session_id": "sess_abc123def4567890",
      "status": "pending_authorization",
      "redirect_url": "https://checkout.faira.io/signup?code=...&session_id=sess_abc123def4567890",
      "pending_transaction_id": "22222222-3333-4444-5555-666666666666"
    }
    ```
  </Step>

  <Step title="Redirect the customer">
    Send the customer's browser to the returned `redirect_url`. They log in or
    onboard with Faira, which resolves the pending transaction from `session_id`.
  </Step>

  <Step title="Customer authorizes">
    The customer approves or declines the pending transaction. Faira then updates
    the transaction and **queues a webhook** to your configured `webhook_url`.
  </Step>
</Steps>

<Note>
  `customer_token`, `unique_customer_reference`, and `redirect_url` are all optional.
  Faira returns a `customer_token` for a customer only after their relationship with
  you is verified — store it and send it on future checkouts for a smoother returning
  experience. The full journey (references, tokens, mappings, transaction
  authorization) is covered in the **Checkout Sessions** guide.
</Note>

***

## Step 5 — Configure webhooks

Faira notifies your backend of transaction state changes by `POST`ing a signed JSON
payload to your `webhook_url`. Set the URL when you
[register](#step-1-create-your-merchant-account) (optional `webhook_url` field) or
later via [your profile](#step-3-complete-your-profile). Webhooks are only delivered
once a `webhook_url` is configured.

### Webhook URL requirements

<Check>Must be **HTTPS**.</Check>
<Check>Must **not** be localhost, a private/internal IP, or a blocked consumer domain.</Check>
<Check>Should respond with a `2xx` quickly (Faira's delivery timeout is **10 seconds**).</Check>

### Events

| Event                    | When it fires                                            |
| ------------------------ | -------------------------------------------------------- |
| `transaction.authorized` | Customer authorized the transaction                      |
| `transaction.declined`   | Customer declined the transaction                        |
| `transaction.paid`       | Checkout transaction was paid (card or SteadyPay)        |
| `transaction.paid_out`   | Merchant payout completed *(reserved — not yet emitted)* |

### Delivery headers

Every delivery includes:

```http theme={null}
Content-Type: application/json
Faira-Webhook-Event: transaction.paid
Faira-Webhook-Timestamp: 1783764000
Faira-Webhook-Signature: sha256=<hex-hmac>
Faira-Webhook-Delivery-ID: <uuid>
```

### Example payload

```json theme={null}
{
  "event": "transaction.paid",
  "transaction_id": "22222222-3333-4444-5555-666666666666",
  "merchant_id": "11111111-2222-3333-4444-555555555555",
  "customer_reference": "cust_7890",
  "status": "paid",
  "amount": 10000,
  "currency": "GBP",
  "provider": "acquired",
  "payment_transaction_id": "33333333-4444-5555-6666-777777777777",
  "authorized_at": "2026-07-11T10:00:00Z",
  "paid_at": "2026-07-11T10:02:00Z"
}
```

<Note>
  Some fields appear only when available. Legacy authorization/decline deliveries may
  omit `provider` and `payment_transaction_id`.
</Note>

### Verifying the signature

The signature is `HMAC-SHA256` of `timestamp` + `"."` + `raw_body`, using your
**webhook signing secret** (`fai_whsec_...`), prefixed with `sha256=`.

<CodeGroup>
  ```js Node.js theme={null}
  import crypto from "node:crypto";

  function verifyFairaWebhook(secret, req) {
    const timestamp = req.headers["faira-webhook-timestamp"];
    const signature = req.headers["faira-webhook-signature"];
    const rawBody = req.rawBody; // the raw bytes, NOT a re-serialized object

    const expected =
      "sha256=" +
      crypto.createHmac("sha256", secret)
        .update(timestamp + "." + rawBody)
        .digest("hex");

    const ok =
      signature &&
      crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));

    // Reject stale deliveries to limit replay risk
    const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
    return ok && ageSeconds < 300;
  }
  ```

  ```python Python theme={null}
  import hmac, hashlib, time

  def verify_faira_webhook(secret: str, timestamp: str, signature: str, raw_body: bytes) -> bool:
      mac = hmac.new(secret.encode(), (timestamp + ".").encode() + raw_body, hashlib.sha256)
      expected = "sha256=" + mac.hexdigest()
      if not hmac.compare_digest(expected, signature):
          return False
      return abs(time.time() - int(timestamp)) < 300  # reject stale timestamps
  ```

  ```go Go theme={null}
  func verifyFairaWebhook(secret, timestamp, signature string, body []byte) bool {
  	mac := hmac.New(sha256.New, []byte(secret))
  	mac.Write([]byte(timestamp))
  	mac.Write([]byte("."))
  	mac.Write(body)
  	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
  	return hmac.Equal([]byte(expected), []byte(signature))
  }
  ```
</CodeGroup>

<Warning>
  Verify against the **raw request body bytes**, not a re-serialized object. Any
  whitespace or key-ordering change breaks the HMAC. Also reject timestamps more than
  \~5 minutes old to reduce replay risk.
</Warning>

### Retries & delivery

Faira treats any `2xx` as delivered. Non-`2xx` responses, network errors, and
timeouts are retried with exponential backoff:

| Attempt | Delay before retry |
| ------- | ------------------ |
| 1       | 1 minute           |
| 2       | 2 minutes          |
| 3       | 4 minutes          |
| 4       | 8 minutes          |
| 5       | 16 minutes         |

The maximum number of attempts is **5** by default. A delivery moves through these
statuses: `queued` → `processing` → `delivered`, or `retrying` → `failed` once the
retry limit is reached.

<Warning>
  **Make your handler idempotent.** A network failure after your server processes a
  delivery but before Faira reads your `2xx` will cause a redelivery. Deduplicate on
  `transaction_id` + `event` (or the `Faira-Webhook-Delivery-ID` header).
</Warning>

### Inspect your webhook deliveries

You can review the outbound webhooks Faira has sent you — useful for debugging a
missed or failing delivery. These endpoints require a **dashboard session**
(`fai_mst_...`).

```http theme={null}
GET /api/v1/merchants/webhooks
GET /api/v1/merchants/webhooks/{webhook_event_id}
Authorization: Bearer fai_mst_...
```

**Query parameters** (list endpoint):

| Param      | Description                                                                           |
| ---------- | ------------------------------------------------------------------------------------- |
| `status`   | Filter by delivery status (`queued`, `processing`, `delivered`, `retrying`, `failed`) |
| `event`    | Filter by event name (e.g. `transaction.paid`)                                        |
| `page`     | Page number (default `1`)                                                             |
| `per_page` | Items per page (default `25`)                                                         |

```json 200 OK — list theme={null}
{
  "webhooks": [
    {
      "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
      "direction": "outbound",
      "source": "faira",
      "event": "transaction.paid",
      "transaction_id": "22222222-3333-4444-5555-666666666666",
      "url": "https://acme.example/webhooks/faira",
      "status": "delivered",
      "attempts": 1,
      "last_error": null,
      "delivered_at": "2026-07-11T10:02:01Z",
      "created_at": "2026-07-11T10:02:00Z",
      "payload": { "event": "transaction.paid", "amount": 10000, "currency": "GBP" }
    }
  ],
  "pagination": { "page": 1, "per_page": 25, "total": 1 }
}
```

The detail endpoint returns a single event wrapped as `{ "webhook": { ... } }`. Use
`status`, `attempts`, and `last_error` to diagnose failures.

### Inbound payment events (optional)

If you settle payments yourself, you can notify Faira of payment/payout events with
a signed request to:

```http theme={null}
POST /api/v1/webhooks/merchants/payment-events
```

Headers:

```http theme={null}
X-Merchant-Id: <merchant_id>
X-Webhook-Timestamp: <unix-or-rfc3339 timestamp>
X-Webhook-Signature: sha256=<hex-hmac>
```

The signature covers `timestamp.raw_json_body` using your webhook signing secret —
the same algorithm Faira uses for outbound deliveries.

```json Request theme={null}
{
  "event_id": "merchant_evt_123",
  "event": "payment.paid",
  "transaction_id": "22222222-3333-4444-5555-666666666666",
  "merchant_ref": "order_1234",
  "paid_at": "2026-07-11T10:02:00Z",
  "metadata": { "channel": "bank_transfer" }
}
```

```json 202 Accepted theme={null}
{
  "status": "accepted",
  "event_id": "merchant_evt_123",
  "event": "transaction.paid",
  "transaction_id": "22222222-3333-4444-5555-666666666666",
  "duplicate": false
}
```

Provide either `transaction_id` (Faira's ID) or `merchant_ref` (your reference).
Events are deduplicated by `event_id`; a duplicate returns `202` with
`"duplicate": true`. Accepted event names: `payment.paid` / `transaction.paid` /
`paid`, and `payment.paid_out` / `payout.paid_out` / `paid_out` / `settled`.

***

## Error handling

Errors use a consistent JSON envelope:

```json theme={null}
{
  "error": {
    "code": "login_failed",
    "message": "invalid email or password"
  }
}
```

| Status | Typical cause                                                            |
| ------ | ------------------------------------------------------------------------ |
| `400`  | Malformed JSON or invalid parameters                                     |
| `401`  | Missing/invalid credentials, bad HMAC signature, or expired session      |
| `404`  | Resource not found                                                       |
| `409`  | Conflict (e.g. duplicate organization name)                              |
| `422`  | Validation error (e.g. passwords don't match)                            |
| `429`  | Rate limited (registration, login, and password endpoints are throttled) |

***

## Go-live checklist

<Steps>
  <Step title="Credentials stored securely">
    API key, API secret, and webhook signing secret are in your secrets manager —
    not in source control.
  </Step>

  <Step title="Profile complete">
    Settlement account, contact details, and a production `webhook_url` are set.
  </Step>

  <Step title="HMAC signing verified">
    A signed `POST /api/v1/sessions/initiate` returns `201` in sandbox.
  </Step>

  <Step title="Webhook endpoint live">
    HTTPS, publicly reachable, verifies signatures, is idempotent, and replies
    `2xx` within 10 seconds.
  </Step>

  <Step title="Redirect handling">
    Your `redirect_url` correctly resumes the customer after the Faira flow.
  </Step>

  <Step title="Idempotency + retries handled">
    Duplicate deliveries are safely ignored; failed webhooks are reconciled.
  </Step>
</Steps>

Need help? Contact your Faira representative or email support with your
`merchant_id` (never share your API secret or webhook signing secret).
