REST API

# Checkout Sessions API

Create one-off hosted checkout sessions programmatically. Your server creates a session, redirects the customer to the returned checkout URL, and receives a webhook when payment completes.

## When to use checkout sessions

Checkout sessions are for application-driven checkout flows: an order, cart, invoice, or booking exists in your system, your server creates a matching Manza session, and the customer pays on a hosted Manza page. Use them when you want to attach internal identifiers in `metadata` and get them back in webhooks, redirect the customer back to your app after payment, and keep card collection and API keys off your frontend.

Reach for [payment links](https://ma.manza.dev/docs/api/payment-links) instead when you need a shareable link that can be managed, reused, or sent manually outside a checkout flow.

## Integration flow

1. Create an order in your system.
2. Create a Manza checkout session from your server.
3. Redirect the customer to the returned `url`.
4. Listen for `checkout_session.completed`.
5. Fulfill the order after the webhook is received.

> **Do not fulfill on the success_url redirect:** Browser redirects can be interrupted, retried, or opened manually. Use the `checkout_session.completed` webhook as the canonical fulfillment signal. The `{CHECKOUT_SESSION_ID}` token in `success_url` lets you query the session for immediate status feedback when the customer returns — but keep the webhook as the source of truth for fulfillment.

## Returning customers to your mobile app

After a successful payment, the customer briefly sees the hosted confirmation page and is then automatically redirected to the URL configured in `success_url`. The redirect uses client-side navigation, which routes HTTPS URLs through the browser and custom-scheme URLs through the OS deep-link handler.

If the customer paid inside your mobile app — an in-app browser or a system browser launched from the app — there are three options for where to send them. Listed best to worst:

1. **Universal Link / App Link (recommended). **A plain HTTPS URL on your own domain that the OS routes to your installed app, falling back to the web if it isn't installed. Configure `apple-app-site-association` (iOS) and `assetlinks.json` (Android) on the same domain — one URL works on mobile and desktop, with no broken-redirect failure mode.
2. **Dispatcher page on your own domain. **An HTTPS URL that runs JavaScript on load to redirect to your app's custom scheme, falling back to the App Store or Play Store when no scheme handler fires. Useful for a conversion-tracking touchpoint before the OS-level hand-off.
3. **Custom app scheme directly. **A URL like `myapp://payment-success` that the OS hands to your installed app. Works only if the app is installed; in-app browsers (Facebook, Instagram, some mail clients) may silently swallow this with no fallback.

```
"success_url": "https://merchant.example.com/payment-success?session={CHECKOUT_SESSION_ID}"
```

> **Schemes rejected for security:** The API accepts all three forms in `success_url` and `cancel_url`, but always rejects `http`, `javascript`, `data`, `file`, `vbscript`, `about`, and `blob` schemes. Deep links can be swallowed by in-app browsers and custom schemes fail when the app isn't installed — always treat the `checkout_session.completed` webhook, never the redirect arriving, as the canonical fulfillment signal.

## Required scopes

| Endpoint | Scope |
| --- | --- |
| Create checkout sessions | `checkout_sessions:write` |
| Get checkout sessions | `checkout_sessions:read` |

## Checkout session object

The session inherits `currency_code` from the destination account. You are viewing the **MAD** deployment, so a checkout session looks like this:

```json
{
  "id": "cs_01964a3b7c8d70008000deadbeef1234",
  "status": "open",
  "amount": "1500.00",
  "currency_code": "MAD",
  "success_url": "https://merchant.example.com/success?session=cs_01964a3b7c8d70008000deadbeef1234",
  "cancel_url": "https://merchant.example.com/cancel",
  "customer_email": "customer@example.com",
  "metadata": {
    "order_id": "ORD-123",
    "user_id": "usr_456"
  },
  "description": "Order #123",
  "url": "https://ma.manza.dev/checkout/cs_01964a3b7c8d70008000deadbeef1234",
  "expires_at": "2026-03-15T09:30:00Z",
  "completed_at": null,
  "settled_at": null,
  "transaction": null,
  "created_at": "2026-03-15T09:00:00Z"
}
```

> **Effective status:** The `status` field is effective status, not raw database status. An expired session with an in-flight card payment can be returned as `processing` until provider confirmation arrives. A confirmed payment reads `clearing` until the provider settles, then `complete` with `settled_at` set.

## Create checkout session

`POST` `/api/checkout_sessions`

Required fields: `account_id`, `amount`, and `success_url`. Discover valid account IDs via [GET /api/accounts](https://ma.manza.dev/docs/api/accounts). The account must be open and belong to your entity. `{CHECKOUT_SESSION_ID}` in `success_url` is resolved to the session ID before redirect. `metadata` accepts at most 50 keys (letters, numbers, underscores; values ≤ 500 chars).

> **Amount & expiry limits:** Sessions must be for at least **10 MAD** and greater than 0; there is no API-side maximum (per-payer KYC-tier caps apply when the card is charged). `expires_at` defaults to 30 minutes from creation and can be set up to 7 days ahead.

cURL

CLI

JavaScript

Ruby

Python

Go

Rust

Elixir

Crystal

PHP

```console
curl -X POST "https://ma.manza.dev/api/checkout_sessions" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "account_id": "01964a3b-0000-7000-8000-ac6000000a01",
    "amount": "1500.00",
    "success_url": "https://merchant.example.com/success?session={CHECKOUT_SESSION_ID}",
    "cancel_url": "https://merchant.example.com/cancel",
    "customer_email": "customer@example.com",
    "metadata": { "order_id": "ORD-123", "user_id": "usr_456" },
    "description": "Order #123"
  }'
```

```console
manza checkout-sessions create \
  --account-id 01964a3b-0000-7000-8000-ac6000000a01 \
  --amount 1500.00 \
  --success-url "https://merchant.example.com/success?session={CHECKOUT_SESSION_ID}"
```

```javascript
import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });

await manza.checkoutSessions.create({
  account_id: "01964a3b-0000-7000-8000-ac6000000a01",
  amount: "1500.00",
  success_url: "https://merchant.example.com/success?session={CHECKOUT_SESSION_ID}",
  cancel_url: "https://merchant.example.com/cancel",
  customer_email: "customer@example.com",
  metadata: { order_id: "ORD-123", user_id: "usr_456" },
  description: "Order #123",
});
```

```ruby
require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])

manza.checkout_sessions.create(
  account_id: "01964a3b-0000-7000-8000-ac6000000a01",
  amount: "1500.00",
  success_url: "https://merchant.example.com/success?session={CHECKOUT_SESSION_ID}",
  cancel_url: "https://merchant.example.com/cancel",
  customer_email: "customer@example.com",
  metadata: { order_id: "ORD-123", user_id: "usr_456" },
  description: "Order #123"
)
```

```python
from manza import Manza
client = Manza()  # reads MANZA_API_KEY

client.checkout_sessions.create(
    account_id="01964a3b-0000-7000-8000-ac6000000a01",
    amount="1500.00",
    success_url="https://merchant.example.com/success?session={CHECKOUT_SESSION_ID}",
    cancel_url="https://merchant.example.com/cancel",
    customer_email="customer@example.com",
    metadata={"order_id": "ORD-123", "user_id": "usr_456"},
    description="Order #123",
)
```

```go
import manza "github.com/getmanza/manza-go"

client, _ := manza.New(manza.WithAPIKey(os.Getenv("MANZA_API_KEY")))

session, err := client.CheckoutSessions.Create(ctx, manza.Attributes{
    "account_id":     "01964a3b-0000-7000-8000-ac6000000a01",
    "amount":         "1500.00",
    "success_url":    "https://merchant.example.com/success?session={CHECKOUT_SESSION_ID}",
    "cancel_url":     "https://merchant.example.com/cancel",
    "customer_email": "customer@example.com",
    "metadata":       map[string]any{"order_id": "ORD-123", "user_id": "usr_456"},
    "description":    "Order #123",
})
```

```rust
let client = manza::Client::builder()
    .api_key(std::env::var("MANZA_API_KEY")?)
    .build()?;

let session = client.checkout_sessions().create(&serde_json::json!({
    "account_id": "01964a3b-0000-7000-8000-ac6000000a01",
    "amount": "1500.00",
    "success_url": "https://merchant.example.com/success?session={CHECKOUT_SESSION_ID}",
    "cancel_url": "https://merchant.example.com/cancel",
    "customer_email": "customer@example.com",
    "metadata": { "order_id": "ORD-123", "user_id": "usr_456" },
    "description": "Order #123",
}))?;
```

```elixir
{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))

{:ok, session} =
  Manza.CheckoutSessions.create(client, %{
    "account_id" => "01964a3b-0000-7000-8000-ac6000000a01",
    "amount" => "1500.00",
    "success_url" => "https://merchant.example.com/success?session={CHECKOUT_SESSION_ID}",
    "cancel_url" => "https://merchant.example.com/cancel",
    "customer_email" => "customer@example.com",
    "metadata" => %{"order_id" => "ORD-123", "user_id" => "usr_456"},
    "description" => "Order #123"
  })
```

```crystal
require "manza"

client = Manza::Client.new # reads MANZA_API_KEY

session = client.checkout_sessions.create(
  account_id: "01964a3b-0000-7000-8000-ac6000000a01",
  amount: "1500.00",
  success_url: "https://merchant.example.com/success?session={CHECKOUT_SESSION_ID}",
  cancel_url: "https://merchant.example.com/cancel",
  customer_email: "customer@example.com",
  metadata: {order_id: "ORD-123", user_id: "usr_456"},
  description: "Order #123"
)
```

```php
use Manza\Client;

$client = new Client(apiKey: getenv('MANZA_API_KEY'));

$session = $client->checkoutSessions->create([
    'account_id' => '01964a3b-0000-7000-8000-ac6000000a01',
    'amount' => '1500.00',
    'success_url' => 'https://merchant.example.com/success?session={CHECKOUT_SESSION_ID}',
    'cancel_url' => 'https://merchant.example.com/cancel',
    'customer_email' => 'customer@example.com',
    'metadata' => ['order_id' => 'ORD-123', 'user_id' => 'usr_456'],
    'description' => 'Order #123',
]);
```

```json
{
  "id": "cs_01964a3b7c8d70008000deadbeef1234",
  "status": "open",
  "amount": "1500.00",
  "currency_code": "MAD",
  "success_url": "https://merchant.example.com/success?session=cs_01964a3b7c8d70008000deadbeef1234",
  "cancel_url": "https://merchant.example.com/cancel",
  "customer_email": "customer@example.com",
  "metadata": {
    "order_id": "ORD-123",
    "user_id": "usr_456"
  },
  "description": "Order #123",
  "url": "https://ma.manza.dev/checkout/cs_01964a3b7c8d70008000deadbeef1234",
  "expires_at": "2026-03-15T09:30:00Z",
  "completed_at": null,
  "settled_at": null,
  "transaction": null,
  "created_at": "2026-03-15T09:00:00Z"
}
```

Redirect your customer to the returned `url`.

## Get checkout session

`GET` `/api/checkout_sessions/:id`

Returns the current checkout session state. Use the session ID from the `id` field (`cs_...`).

cURL

CLI

JavaScript

Ruby

Python

Go

Rust

Elixir

Crystal

PHP

```console
curl "https://ma.manza.dev/api/checkout_sessions/cs_01964a3b7c8d70008000deadbeef1234" \
  -H "Authorization: Bearer sk_live_..."
```

```console
manza checkout-sessions get cs_01964a3b7c8d70008000deadbeef1234
```

```javascript
import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });

await manza.checkoutSessions.get("cs_01964a3b7c8d70008000deadbeef1234");
```

```ruby
require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])

manza.checkout_sessions.get("cs_01964a3b7c8d70008000deadbeef1234")
```

```python
from manza import Manza
client = Manza()  # reads MANZA_API_KEY

client.checkout_sessions.get("cs_01964a3b7c8d70008000deadbeef1234")
```

```go
import manza "github.com/getmanza/manza-go"

client, _ := manza.New(manza.WithAPIKey(os.Getenv("MANZA_API_KEY")))

session, err := client.CheckoutSessions.Get(ctx, "cs_01964a3b7c8d70008000deadbeef1234")
```

```rust
let client = manza::Client::builder()
    .api_key(std::env::var("MANZA_API_KEY")?)
    .build()?;

let session = client.checkout_sessions().get("cs_01964a3b7c8d70008000deadbeef1234")?;
```

```elixir
{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))

{:ok, session} = Manza.CheckoutSessions.get(client, "cs_01964a3b7c8d70008000deadbeef1234")
```

```crystal
require "manza"

client = Manza::Client.new # reads MANZA_API_KEY

session = client.checkout_sessions.get("cs_01964a3b7c8d70008000deadbeef1234")
```

```php
use Manza\Client;

$client = new Client(apiKey: getenv('MANZA_API_KEY'));

$session = $client->checkoutSessions->get('cs_01964a3b7c8d70008000deadbeef1234');
```

## Session lifecycle

Sessions expire automatically after 30 minutes by default; use `expires_at` to shorten or extend that window. Checkout sessions are immutable after creation — to change the amount, account, description, metadata, or redirect URLs, create a new session.

| Status | Meaning | Merchant action |
| --- | --- | --- |
| `open` | The session can still accept payment. | Wait for the customer to pay or redirect them to url again. |
| `processing` | A payment attempt was submitted; provider confirmation is pending. | Do not fulfill yet. Wait for webhook confirmation or poll the session. |
| `clearing` | Payment was confirmed and the merchant account credited; provider settlement is pending. | Fulfill the order idempotently — the payment will not be asked for again. settled_at is still null. |
| `complete` | The provider settled the payment. | Nothing further — settled_at is set and a checkout_session.settled webhook fired. |
| `expired` | The session can no longer accept payment. | Create a new checkout session if the customer still needs to pay. |

## Webhooks

When payment completes, a `checkout_session.completed` webhook fires. The payload includes the session data — `metadata` and `customer_email` included. The nested `transaction` includes the gross amount, actual `fee_amount`, net amount, card summary, provider reference, and account transaction ID. Use it to mark the order paid and start fulfillment. The session `status` in this payload may be `clearing` or `complete`: fulfill on the event, and do not wait for `complete`. Handlers must be idempotent: store the session `id` or your `metadata.order_id` and ignore duplicate completion events for already-fulfilled orders. A `checkout_session.settled` webhook follows once the provider settles the funds (`settled_at` set, status `complete`). See the [Webhooks documentation](https://ma.manza.dev/docs/webhooks) for signature verification and retry policy.

```json
{
  "event": "checkout_session.completed",
  "created_at": "2026-03-15T09:05:00Z",
  "data": {
    "checkout_session": {
      "id": "cs_01964a3b7c8d70008000deadbeef1234",
      "status": "clearing",
      "amount": "1500.00",
      "currency_code": "MAD",
      "success_url": "https://merchant.example.com/success?session=cs_01964a3b7c8d70008000deadbeef1234",
      "cancel_url": "https://merchant.example.com/cancel",
      "customer_email": "customer@example.com",
      "metadata": {
        "order_id": "ORD-123",
        "user_id": "usr_456"
      },
      "description": "Order #123",
      "url": "https://ma.manza.dev/checkout/cs_01964a3b7c8d70008000deadbeef1234",
      "expires_at": "2026-03-15T09:30:00Z",
      "completed_at": "2026-03-15T09:05:00Z",
      "settled_at": null,
      "transaction": {
        "id": "01964a3b-7c8d-7000-8000-deadbeef1234",
        "status": "accepted",
        "amount": "1500.00",
        "fee_amount": "15.0",
        "net_amount": "1485.0",
        "currency_code": "MAD",
        "payment_reference": null,
        "provider_reference": "PAY-2026-001",
        "card_brand": "Visa",
        "card_last_four": "4242",
        "account_transaction_id": "01964a3b-7c8d-7000-8000-feedface1234",
        "processed_at": "2026-03-15T09:04:58Z",
        "created_at": "2026-03-15T09:04:00Z"
      },
      "created_at": "2026-03-15T09:00:00Z"
    }
  }
}
```

## Error responses

| Scenario | Status | Type | Param |
| --- | --- | --- | --- |
| Missing or invalid API key | 401 | `authentication_error` | — |
| API access not enabled for the entity | 403 | `forbidden_error` | — |
| Missing required checkout session scope | 403 | `insufficient_scope` | — |
| Session not found or belongs to another entity | 404 | `not_found_error` | — |
| Invalid account_id | 422 | `validation_error` | `account_id` |
| Destination account is not open | 422 | `validation_error` | `account_id` |
| Amount ≤ 0 | 422 | `validation_error` | `amount` |
| Amount below 10 MAD minimum | 422 | `validation_error` | `amount` |
| Missing required field | 400 | `invalid_request_error` | `account_id, amount, or success_url` |
| Invalid success_url or cancel_url | 422 | `validation_error` | `success_url or cancel_url` |
| expires_at in the past | 422 | `validation_error` | `expires_at` |
| expires_at more than 7 days ahead | 422 | `validation_error` | `expires_at` |
| Invalid metadata type or limits | 422 | `validation_error` | `metadata` |