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

# How it works (end-to-end)

> Every endpoint a merchant calls, in order, to take a standard or milestone payment from creation to settlement.

This is the **map of the whole journey**. It shows every endpoint you call, in the
order you call it, and who the `customer-id` header points to at each step. Read
it once and the individual [API reference](/api-reference/escrow/create-escrow)
pages will fall into place.

<Info>
  Two actors run through every flow:

  * **Seller**: the merchant customer selling the item (the escrow is created *on their behalf*).
  * **Buyer**: the merchant customer funding the escrow.

  You, the **merchant**, hold the secret key and act on behalf of both by setting
  the **`customer-id`** header to whichever one the current step concerns. The
  secret key (`Authorization: Bearer sk_…`) is on **every** request.
</Info>

## The journey at a glance

<Steps>
  <Step title="Set up (once)">
    Get your secret key, and optionally seed categories and look up a `countryId`.
  </Step>

  <Step title="Onboard your customers">
    Create the buyer and seller as **merchant customers**.
  </Step>

  <Step title="Create the deal">
    Create a **standard** escrow or a **milestone** escrow. You get a `paymentToken` and an `id`.
  </Step>

  <Step title="Fund it">
    The buyer pays into escrow. Funds are held, not released.
  </Step>

  <Step title="Deliver & release">
    The buyer confirms (or each milestone is confirmed), releasing funds net of fees.
  </Step>

  <Step title="Handle exceptions">
    Claims, disputes and refunds (only if the happy path doesn't complete).
  </Step>
</Steps>

***

## Phase 0: Set up (once per merchant)

| #   | Call                                                                                                                        | Endpoint                            | Notes                                                 |
| --- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------- |
| 0.1 | [Get countries](/api-reference/misc/get-countries)                                                                          | `GET /v1/misc/countries`            | Returns the `countryId` you need to create customers. |
| 0.2 | [List categories](/api-reference/categories/list-categories) / [Create category](/api-reference/categories/create-category) | `GET` / `POST /v1/escrow/category…` | Optional. Group your escrows into a catalog.          |

These don't move money, so do them once and cache the IDs.

***

## Phase 1: Onboard your customers

Almost every later call needs a `customer-id`, so you create the people first.

| #   | Call                                                                                            | Endpoint                                    | `customer-id`          |
| --- | ----------------------------------------------------------------------------------------------- | ------------------------------------------- | ---------------------- |
| 1.1 | [Create merchant customer](/api-reference/merchant-customers/create-merchant-customer) (seller) | `POST /v1/customer/create`                  | none (super-admin key) |
| 1.2 | [Create merchant customer](/api-reference/merchant-customers/create-merchant-customer) (buyer)  | `POST /v1/customer/create`                  | none (super-admin key) |
| 1.3 | [Update permissions](/api-reference/merchant-customers/update-customer-permissions)             | `PUT /v1/customer/permissions/{customerId}` | none                   |

<Check>The seller needs `canSell`, the buyer needs `canBuy`. A buyer with `canBuy` disabled is rejected at payment time.</Check>

Keep the returned customer IDs; they become the `customer-id` header for the rest
of the flow.

***

## Phase 2: The standard escrow flow

A single item, paid once, released once. This is the common case.

```mermaid theme={null}
flowchart LR
  A["Create escrow<br/>(seller)"] --> B["Buyer verifies<br/>token"]
  B --> C["Buyer funds<br/>escrow"]
  C --> D{"Delivered?"}
  D -->|Buyer confirms| E["Released → seller<br/>COMPLETED"]
  D -->|Window elapsed| F["Seller claims<br/>CLAIMED"]
  D -->|Problem| G["Dispute → merchant<br/>resolves"]
```

<Steps>
  <Step title="Create the escrow (as the seller)">
    [`POST /v1/escrow/create`](/api-reference/escrow/create-escrow) with
    `customer-id: <sellerId>`. This is **`multipart/form-data`** (so you can attach
    up to 5 images).

    ```bash theme={null}
    curl -X POST https://staging.api.payluk.ng/v1/escrow/create \
      -H "Authorization: Bearer sk_test_…" \
      -H "customer-id: <sellerId>" \
      -F "amount=150000" \
      -F "purpose=MacBook Pro 14\"" \
      -F "whoPays=both" \
      -F "maxDelivery=3" \
      -F "deliveryTimeline=days" \
      -F "totalQuantity=1"
    ```

    Returns `id` and `paymentToken`; `state: AWAITING_PAYMENT`, `status: PENDING`,
    `settlementType: STANDARD`. While in `AWAITING_PAYMENT` you can still
    [edit](/api-reference/escrow/edit-escrow) or
    [delete](/api-reference/escrow/delete-escrow) it.

    <Warning>Selling **more than one unit** off this single link? Set `totalQuantity` above 1 and read [Multi-quantity escrows](/concepts/multi-quantity-escrows); the behaviour is different and is explained there.</Warning>
  </Step>

  <Step title="Buyer resolves the link (optional)">
    Share the `paymentToken`. The buyer's side can read the full escrow with
    [`GET /v1/escrow/verify/{paymentToken}`](/api-reference/escrow/verify-payment-token)
    to display amount, purpose and images before paying.
  </Step>

  <Step title="Make sure the buyer's wallet is funded">
    Paying with `gateway: WALLET` requires balance. Top it up first:

    * **Production:** [`POST /v1/payment/create-intent`](/api-reference/payments/create-payment-intent)
      (`transactionType: DEPOSIT`) → returns an `authorizationUrl` → buyer pays →
      [`POST /v1/payment/verify`](/api-reference/payments/verify-payment).
    * **Staging shortcut:** [`POST /v1/payment/topup`](/api-reference/payments/top-up-virtual-account-staging-only)
      with `customer-id: <buyerId>`.

    Skip this step if you fund the escrow directly with a `PAYSTACK`/`FLUTTERWAVE` gateway.
  </Step>

  <Step title="Fund the escrow (as the buyer)">
    [`POST /v1/payment/escrow`](/api-reference/payments/pay-escrow-buy) with
    `customer-id: <buyerId>`.

    ```bash theme={null}
    curl -X POST https://staging.api.payluk.ng/v1/payment/escrow \
      -H "Authorization: Bearer sk_test_…" \
      -H "customer-id: <buyerId>" \
      -H "Content-Type: application/json" \
      -d '{
        "gateway": "WALLET",
        "transactionType": "ESCROW",
        "reference": "ESC_REF_98765",
        "escrowDetails": { "escrowId": "<escrowId>" }
      }'
    ```

    The buyer is charged the **escrow amount + their share of the fee**. The escrow
    moves to `state: OPENED` / `status: ONGOING` and the delivery window starts.

    <Info>With a non-wallet gateway this returns an `authorizationUrl`; after the buyer pays, call [`POST /v1/payment/verify`](/api-reference/payments/verify-payment) to settle it into escrow.</Info>
  </Step>

  <Step title="Release the funds">
    The happy path: the buyer confirms delivery:
    [`POST /v1/escrow/confirm-payment/{escrowId}`](/api-reference/disputes/buyer-confirm-payment-standard)
    with `customer-id: <buyerId>`. The escrow closes as `COMPLETED` and the seller
    is paid net of fee.
  </Step>
</Steps>

### If the happy path doesn't happen

<AccordionGroup>
  <Accordion title="Buyer never confirms → seller claims">
    After the delivery window elapses, the seller calls
    [`GET /v1/escrow/claim-funds/{paymentToken}`](/api-reference/escrow/claim-funds)
    (`customer-id: <sellerId>`). Allowed only when `OPENED` and the window has
    passed. Closes as `CLAIMED`.
  </Accordion>

  <Accordion title="Something's wrong → dispute">
    Either party opens
    [`POST /v1/escrow/submit-dispute/{paymentToken}`](/api-reference/disputes/submit-dispute).
    You, the merchant, then resolve it with
    [`POST /v1/escrow/dispute/resolve/{escrowId}`](/api-reference/disputes/resolve-dispute):
    release to the seller or refund the buyer (`REFUNDED`).
    <Warning>The resolve and feeds routes are merchant-wide; **do not** send a `customer-id` header.</Warning>
  </Accordion>
</AccordionGroup>

***

## Phase 3: The milestone escrow flow

Fund the **whole** project upfront; release it **in stages** as each milestone is
confirmed. Same money rails, different create + release steps.

```mermaid theme={null}
flowchart LR
  A["Create milestone escrow<br/>(seller)"] --> B["Buyer funds<br/>in full"]
  B --> C["Confirm milestone 1<br/>→ net share released"]
  C --> D["Confirm milestone 2<br/>→ net share released"]
  D --> E["Confirm final milestone<br/>→ auto COMPLETED"]
```

<Steps>
  <Step title="Create the milestone escrow (as the seller)">
    [`POST /v1/escrow/milestone/create`](/api-reference/milestone-escrow/create-milestone-escrow)
    with `customer-id: <sellerId>`. Unlike standard create, this is **JSON** (no
    file upload).

    ```json theme={null}
    {
      "amount": 1000000,
      "purpose": "Company website build",
      "whoPays": "buyer",
      "milestones": [
        { "title": "Design",      "amount": 300000, "dueDate": "2026-07-15" },
        { "title": "Development", "amount": 500000, "dueDate": "2026-08-15" },
        { "title": "Deployment",  "amount": 200000 }
      ]
    }
    ```

    <Check>At least **2** milestones.</Check>
    <Check>Milestone amounts must **sum to** the escrow `amount`.</Check>
    <Check>`whoPays` must be **`buyer`** on milestone escrows.</Check>

    Returns `settlementType: MILESTONE` with a `milestones[]` array; each milestone
    has its own `id` and `status: PENDING`.
  </Step>

  <Step title="Fund it in full (as the buyer)">
    Same endpoint as standard:
    [`POST /v1/payment/escrow`](/api-reference/payments/pay-escrow-buy) with
    `customer-id: <buyerId>`. The buyer pays the **entire** amount once. Escrow →
    `OPENED`.
  </Step>

  <Step title="Confirm each milestone (as the buyer)">
    Read the current milestones any time with
    [`GET /v1/escrow/milestone/{paymentToken}`](/api-reference/milestone-escrow/get-milestones).
    As each deliverable lands, the buyer confirms it:
    [`POST /v1/escrow/milestone/confirm/{escrowId}/{milestoneId}`](/api-reference/milestone-escrow/confirm-milestone)
    with `customer-id: <buyerId>`. That milestone's **net share** (its amount minus
    its pro-rata fee) is released and its `status` becomes `RELEASED`.
  </Step>

  <Step title="Auto-complete">
    When the **final** milestone is released, the escrow moves to
    `CLOSED / COMPLETED` automatically. No separate confirm-payment call.
  </Step>
</Steps>

***

## Phase 4: Monitor & reconcile (any time)

<Tip>
  Don't poll for status changes; register a callback URL and let Payluk push every
  escrow update to you. See [Escrow webhooks](/concepts/webhooks).
</Tip>

| Call                                                                                                             | Endpoint                               | Use                               |
| ---------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------- |
| [List escrow transactions](/api-reference/escrow/list-escrow-transactions)                                       | `GET /v1/escrow/transactions`          | All escrows and their states.     |
| [Get payment history](/api-reference/payments/get-payment-history)                                               | `GET /v1/payment/history`              | Money in/out per customer.        |
| [Get customer wallet](/api-reference/merchant-customers/get-customer-wallet)                                     | `GET /v1/wallet`                       | `mainBalance` vs `escrowBalance`. |
| [List my disputes](/api-reference/disputes/list-my-disputes) / [feeds](/api-reference/disputes/get-escrow-feeds) | `GET /v1/escrow/dispute/get`, `/feeds` | Dispute activity.                 |

***

## Endpoint checklist

<Tabs>
  <Tab title="Standard payment">
    1. `POST /v1/customer/create`: seller, then buyer *(once)*
    2. `POST /v1/escrow/create` with **`customer-id: seller`**
    3. `GET /v1/escrow/verify/{paymentToken}`: buyer previews *(optional)*
    4. Fund wallet: `POST /v1/payment/create-intent` → `POST /v1/payment/verify` *(or `topup` on staging)*
    5. `POST /v1/payment/escrow` with **`customer-id: buyer`**
    6. `POST /v1/escrow/confirm-payment/{escrowId}` with **`customer-id: buyer`**

    * Exceptions: `GET /v1/escrow/claim-funds/{paymentToken}` · `POST /v1/escrow/submit-dispute/{paymentToken}` · `POST /v1/escrow/dispute/resolve/{escrowId}`
  </Tab>

  <Tab title="Milestone payment">
    1. `POST /v1/customer/create`: seller, then buyer *(once)*
    2. `POST /v1/escrow/milestone/create` with **`customer-id: seller`**
    3. `POST /v1/payment/escrow` with **`customer-id: buyer`** (funds in full)
    4. `GET /v1/escrow/milestone/{paymentToken}`: read milestones
    5. `POST /v1/escrow/milestone/confirm/{escrowId}/{milestoneId}` with **`customer-id: buyer`**, repeat per milestone
    6. Final confirm auto-completes the escrow
  </Tab>
</Tabs>

<Note>
  Selling multiple units off one link? That introduces escrow **duplication**:
  read [Multi-quantity escrows](/concepts/multi-quantity-escrows) next.
</Note>
