Skip to main content

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 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)
In production, redirect URLs must use HTTPS. http://localhost is allowed for development only.

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 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:
// 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.

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)

// 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();
}

Security Best Practices

Validate Redirect Params

When handling the callback, validate that session_id matches a session you created. Don’t trust arbitrary query params.

Use Webhooks

Always process webhooks for escrow state changes. Redirect params are for UX; webhooks are for backend truth.

HTTPS in Production

Use HTTPS for all redirect URLs in production. Never use http:// except for localhost during development.

Protect API Keys

Keep API keys server-side only. Never expose them in client-side code or public repositories.

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 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 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. See the Quick Escrow with Proof guide.

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 instead — both parties can cancel via the API.
For API reference details, see Create Checkout Session and Get Checkout Session.