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

# Checkout Sessions Integration Guide

> Step-by-step guide to integrating DHMAD Checkout Sessions

# Checkout Sessions Integration Guide

Checkout Sessions let you redirect users to dhmad.tn to perform critical escrow actions—signing contracts, accepting payments, completing deliveries, opening disputes, or handling cancellations—without building these flows yourself. This guide walks you through the full integration.

## Introduction

When your application needs a user to perform an escrow action that requires DHMAD's hosted UI (e.g., contract signing, payment, proof upload/review, completion), you:

1. Create a checkout session via the API
2. Redirect the user to the session URL
3. The user completes the action on dhmad.tn
4. DHMAD redirects the user back to your app with the result

This approach is similar to Stripe Checkout: you get a hosted, secure flow with minimal integration effort.

## Step-by-Step Integration

### Step 1: Configure Allowed Redirect URLs

Before creating any checkout sessions, add your redirect URLs in the [Developer Dashboard](https://developer.dhmad.tn/dashboard) under Settings. Only URLs in this list will be accepted as `redirectUrl` when creating sessions.

**Examples:**

* `https://myapp.com/checkout/callback`
* `https://myapp.com/escrow/complete`
* `http://localhost:3000/callback` (for local development)

<Warning>
  In production, redirect URLs must use HTTPS. `http://localhost` is allowed for development only.
</Warning>

### Step 2: Create a Checkout Session via API

Call `POST /api/v1/escrows/:id/sessions` with:

* **action** — The action type (e.g., `sign_contract`, `accept_pay`, `submit_proof`, `review_proof`, `complete`)
* **targetUserEmail** — Email of the user who will perform the action
* **redirectUrl** — Your callback URL (must match an allowed URL)
* **metadata** (optional) — Key-value pairs for your own tracking

You receive `sessionId`, `url`, and `expiresAt`. Redirect the user to `url`.

If the action is already done (e.g. the user has already signed the contract, or payment is already completed), the API returns **400** with an error object: `error.code`, `error.message` (for developers), and `error.user_message` (for end users). Check `error.type === 'business_rule_violation'` and display `error.user_message` in your UI instead of redirecting to checkout. See [Create Checkout Session](/api-reference/checkout-sessions/create-session) for all error codes.

### Step 3: Redirect the User to the Session URL

Send the user to the `url` returned from the create session call. For example:

```javascript theme={null}
// After creating the session
window.location.href = data.url;
// e.g., https://dhmad.tn/checkout/550e8400-e29b-41d4-a716-446655440000
```

The user will see the DHMAD checkout page, log in if needed, and complete the action.

### Step 4: Handle the Redirect Callback

When the user finishes (or abandons) the flow, DHMAD redirects them to your `redirectUrl` with query parameters:

* `session_id` — The session ID
* `status` — One of: `completed`, `expired`, `cancelled`

Example callback URL:

```
https://myapp.com/checkout/callback?session_id=550e8400-e29b-41d4-a716-446655440000&status=completed
```

Parse these parameters and update your UI. Optionally call `GET /api/v1/sessions/:sessionId` to verify and get full session details.

### What happens inside checkout

The checkout page shows a summary of the escrow: amount, **platform service fee** (DHMAD's fee, paid by the seller), and, if the escrow was created with a developer fee, the **application / app fee** (with your app name when configured). Both fees are deducted from the seller's payout.

#### Instant escrows — guest payment (`accept_pay`, no DHMAD login)

For escrows created with **`mode: "instant"`** and a checkout session action **`accept_pay`**, if the buyer is **not** logged into DHMAD they can pay **without** creating an account. They pick a payment method (e.g. bank transfer, D17, Flouci, PayPal), complete the payment using the instructions shown, and upload a proof file. An **admin must approve** the proof; only then does the escrow move to `paid` and your app should rely on **`escrow.status.updated`** (and related webhooks) as usual.

* **Funds are not held in a DHMAD buyer wallet** for this path until the proof is approved; cancellation and refunds follow DHMAD’s guest-checkout rules (your app should not assume an automatic wallet refund to the buyer).
* **Sandbox:** the flow may complete without a proof file for faster testing.
* **Seller cancellation before approval:** pending proofs for that escrow are closed automatically; you may receive **`escrow.deposit_proof.rejected`** (with a system reason) and **`escrow.status.updated`** when the escrow becomes `cancelled`. The checkout session may be marked `cancelled`. Treat **webhooks** as the source of truth, not only the browser redirect.

#### Standard flow (logged-in buyer or non-instant escrow)

When the user is on dhmad.tn/checkout and completes **Accept and pay** under the **standard** path (e.g. **standard** escrow mode, or the buyer is signed in and uses the wallet flow), DHMAD may require them to complete additional steps before the action succeeds:

* **Email or phone verification:** If the buyer has not verified their email or phone, they see an inline **popup** on the same page to enter the verification code (OTP). We send the code when the popup opens; after they enter it and verify, they can click **Confirm & Pay** again. No redirect to profile.
* **KYC (identity verification):** If the buyer has not completed KYC, they see an inline **Verify identity (KYC)** block on the same page. They can start verification (opens in a new tab); after completing it, they return to the checkout page and can click **Confirm & Pay** again. No redirect away from checkout.
* **Insufficient balance:** If the buyer's balance is too low, they see an inline top-up option on the same page. After topping up, they click **Confirm & Pay** again.

The user is only redirected back to your `redirectUrl` with `session_id` and `status` when they **complete** the action, **cancel**, or the session **expires**. You do **not** receive error codes (e.g. `KYC_REQUIRED`) in your redirect callback—those cases are handled entirely on DHMAD.

For all error codes returned when **creating** a session (e.g. contract already signed, payment already done, **`SELLER_NOT_READY`** when the seller has not completed pre-account identity verification), see [Create Checkout Session — Error Responses](/api-reference/checkout-sessions/create-session#error-responses).

### Step 5: Verify with Webhooks

Treat webhooks as the source of truth. When a user completes an action via a Checkout Session, DHMAD emits webhooks for the underlying events (e.g., `contract.signed`, `escrow.paid`, `escrow.completed`). Use webhooks to update your backend state; use the redirect for immediate UI feedback.

## Complete Code Example (Node.js / Express)

<CodeGroup>
  ```javascript JavaScript theme={null}
  // Create checkout session and redirect user
  app.post('/create-checkout', async (req, res) => {
    const { escrowId, action, targetUserEmail, orderId } = req.body;

    const response = await fetch(
      `https://dhmad.tn/api/v1/escrows/${escrowId}/sessions`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.DHMAD_API_KEY}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          action,
          targetUserEmail,
          redirectUrl: `${process.env.APP_URL}/checkout/callback`,
          metadata: orderId ? { order_id: orderId } : undefined
        })
      }
    );

    const data = await response.json();
    if (!response.ok) {
      // Handle business rule errors: show user_message to the user instead of redirecting
      if (response.status === 400 && data?.error?.type === 'business_rule_violation') {
        return res.status(400).json({ message: data.error.user_message, code: data.error.code });
      }
      return res.status(response.status).json(data);
    }

    // Redirect user to DHMAD checkout
    res.redirect(302, data.url);
  });

  // Handle redirect callback
  app.get('/checkout/callback', (req, res) => {
    const { session_id, status } = req.query;

    if (status === 'completed') {
      // Optionally verify via API
      // const session = await fetchSession(session_id);
      res.redirect(`/orders/success?session=${session_id}`);
    } else if (status === 'expired') {
      res.redirect('/orders/expired');
    } else {
      res.redirect('/orders/cancelled');
    }
  });

  // Optional: verify session status
  async function fetchSession(sessionId) {
    const response = await fetch(
      `https://dhmad.tn/api/v1/sessions/${sessionId}`,
      {
        headers: { 'Authorization': `Bearer ${process.env.DHMAD_API_KEY}` }
      }
    );
    return response.json();
  }
  ```
</CodeGroup>

## Security Best Practices

<CardGroup cols={2}>
  <Card title="Validate Redirect Params" icon="shield-check">
    When handling the callback, validate that `session_id` matches a session you created. Don't trust arbitrary query params.
  </Card>

  <Card title="Use Webhooks" icon="webhook">
    Always process webhooks for escrow state changes. Redirect params are for UX; webhooks are for backend truth.
  </Card>

  <Card title="HTTPS in Production" icon="lock">
    Use HTTPS for all redirect URLs in production. Never use `http://` except for localhost during development.
  </Card>

  <Card title="Protect API Keys" icon="key">
    Keep API keys server-side only. Never expose them in client-side code or public repositories.
  </Card>
</CardGroup>

## FAQ

### What happens if the session expires?

Sessions expire 30 minutes after creation. If the user does not complete the flow in time, they will be redirected with `status=expired`. Create a new session and redirect the user again if needed.

### Can I create multiple sessions for the same escrow?

Yes. You can create multiple sessions for different actions or for retries. Each session has a unique `sessionId` and is single-use.

### Can the same user complete multiple sessions?

Yes. Each session is independent. A user can complete one session (e.g., sign contract) and later complete another (e.g., accept & pay) for the same escrow.

### What if the user closes the browser before completing?

If the user abandons the flow, they may be redirected with `status=cancelled` or may not reach your callback at all. Handle both cases in your UI. The session remains `pending` until it expires or is completed.

### Do I need to handle the redirect if I use webhooks?

Yes. The redirect provides immediate feedback to the user (e.g., "Payment successful"). Webhooks update your backend. Use both: redirect for UX, webhooks for data integrity.

### Can I use checkout sessions in sandbox mode?

Yes. Use your sandbox API key (`sk_sandbox_*`) and the sandbox base URL. The checkout URL will point to the sandbox frontend (e.g., `https://sandbox.dhmad.tn/checkout/:sessionId`).

### What about instant escrows (digital goods)?

For escrows created with `mode: "instant"` (e.g. in-app coins, AI credits), use only the **accept\_pay** action. The buyer pays in one step; there is no contract to sign and no "complete" step. After the buyer pays, you receive a webhook, deliver the goods, then call the Deliver Escrow API—the escrow auto-completes and funds are released. See [Escrow Modes — Instant](/api-reference/escrows/overview#instant-mode) in the Escrows overview.

### What about quick escrows (products without a contract)?

For escrows created with `mode: "quick"` (e.g. physical or digital products between two users), use **accept\_pay** then **complete** after the seller delivers. There is no contract to sign — `sign_contract` returns 400. See [Escrow Modes — Quick](/api-reference/escrows/overview#quick-mode) in the Escrows overview.

### What about Quick Escrow with Proof?

When you set `fulfillmentPolicy.type: "purchase_proof_required"` at creation, add **`submit_proof`** (seller uploads after pay) and **`review_proof`** (buyer accepts or requests corrections) before the seller can deliver. Fetch proof files with [GET /escrows/:id/proof](/api-reference/escrows/get-escrow-proof). See the [Quick Escrow with Proof guide](/guides/quick-escrow-with-proof).

### How do paid or delivered cancellation checkout sessions work?

After payment, **buyers cannot cancel unilaterally**. Use checkout sessions as follows:

1. **`request_cancellation`** — Target the **buyer's** email. Escrow must be `paid` or `delivered`.
2. **`cancel_escrow`** — Target the **seller's** email. Escrow must be `paid` or `delivered`. Cancels immediately and refunds the buyer when applicable.
3. **`accept_cancellation`** or **`reject_cancellation`** — After the buyer requests, target the **seller's** email so they can accept (refund) or reject.

**Do not mix actions:** targeting the seller for `request_cancellation` or the buyer for `cancel_escrow` returns **400**.

For escrows still in **`pending`** status (before payment), use [Cancel Escrow](/api-reference/escrows/cancel-escrow) instead — both parties can cancel via the API.

***

<Info>
  For API reference details, see [Create Checkout Session](/api-reference/checkout-sessions/create-session) and [Get Checkout Session](/api-reference/checkout-sessions/get-session).
</Info>
