{
  "info": {
    "_postman_id": "a4f4d07c-7c3d-498b-9f27-139babe3500e",
    "name": "Chari Pay API",
    "description": "# Chari Pay API\n\nThe complete, runnable reference for the Chari Pay partner API — every endpoint an\nexternal developer can call, documented inline, in the order you actually need\nthem.\n\n**Start with folder `0 · Get Started`.** You do not need credentials to begin:\nit signs you up for a sandbox account, logs you in, and mints the API key the\nrest of this collection runs on. Eight requests, no e-mails to support.\n\n---\n\n## One base URL, two environments\n\n| Environment | Base URL | Key prefix |\n|---|---|---|\n| Sandbox | `https://api-psp.charipay.ma` | `chari_sk_test_…` |\n| Production | `https://api-psp.charipay.ma` | `chari_sk_live_…` |\n\n**Sandbox and production share the same base URL. The environment is decided by\nyour API key — not by the URL, not by a header, not by a body field.** A sandbox key always operates on sandbox data and a\nproduction key on live data; the two datasets are fully isolated, so the same\n`externalId` may exist once in each.\n\nProduction access additionally requires your account to be production-enabled.\nUntil it is, a production key returns `403 PRODUCTION_ACCESS_NOT_ENABLED`.\n\n\n---\n\n## Authentication\n\nEvery request in this collection except folders `0` and `3` sends:\n\n```\nX-CHARI-PAY-API-KEY: {{apiKey}}\n```\n\nIt is applied at the **collection** level, so there is nothing to set per\nrequest. Keys are issued per enterprise **and** per environment, and each key\ncarries a set of **scopes**; each request below names the scope it needs.\n\nTwo folders deliberately authenticate differently:\n\n- **`0 · Get Started`** — signup and login are public; creating an API key uses\n  your portal **JWT**, not an API key.\n- **`3 · Headless Checkout`** — session-scoped. The single-use verify key (`vk`)\n  is the credential, because it is safe to expose to a browser. **Never send\n  your API key to `/checkout/*`.**\n\n> Treat an API key like a password: server-side only, never in client-side code,\n> never in a repository. The full key is shown exactly once, at creation.\n\n---\n\n## Conventions that apply everywhere\n\n**Money** is in **MAD major units** — `149.90` means 149 dirhams 90 centimes,\nnever centimes. **Timestamps** are ISO-8601 UTC (`2026-08-20T10:15:00Z`).\n\n**Two headers are generated automatically** by this collection's pre-request\nscript, on every send:\n\n| Header | Purpose |\n|---|---|\n| `X-Request-Id` | correlation id, echoed on the response — quote it in support tickets |\n| `Idempotency-Key` | on `POST` only; makes a network retry safe |\n\n**Idempotency has two independent mechanisms.** Use whichever matches your\nfailure mode — or both:\n\n1. **`Idempotency-Key` header** — protects against *network* retries. Replaying\n   the same value returns the first result instead of creating a duplicate.\n2. **`externalId` field** — protects against *your system* re-issuing the same\n   business intent. Creating a resource with an `externalId` that already exists\n   returns the **existing** resource with `200 OK` (a genuinely new create\n   returns `201 Created`). Refunds use `refundReference` for the same purpose.\n\n**Pagination** is `page` (zero-based) + `size`, returning a Spring page\n(`content`, `totalElements`, `totalPages`, `number`, `size`). Transactions also\nsupport cursor paging via `cursor` + `limit`.\n\n**Errors** always use one envelope:\n\n```json\n{\n  \"error\": { \"code\": \"VALIDATION_ERROR\", \"message\": \"amount: must be greater than 0\" },\n  \"correlationId\": \"b0c1e2d3-4f56-7890-abcd-ef0123456789\"\n}\n```\n\nBranch on `code` (stable); show `message` to humans; quote `correlationId` to us.\n\n| Status | Meaning |\n|---|---|\n| `400` | malformed body or failed validation — `MALFORMED_JSON`, `VALIDATION_ERROR`, `MISSING_PARAMETER` |\n| `401` | missing or invalid API key — `UNAUTHORIZED` |\n| `403` | authenticated but missing a scope, or production not enabled — `FORBIDDEN`, `PRODUCTION_ACCESS_NOT_ENABLED` |\n| `404` | not found, or not visible to your account |\n| `409` | conflict, e.g. an idempotency mismatch |\n| `422` | valid, but a business rule blocks it — e.g. `WALLET_NOT_ACTIVE` |\n| `429` | rate limited — `RATE_LIMITED` |\n| `5xx` | our fault only |\n\n---\n\n## Which flow do I want?\n\n| Goal | Path through this collection |\n|---|---|\n| Get paid once, no code | `1 · Payment Links` — share `payUrl` |\n| Checkout inside my own cart | `2 · Checkout Sessions` — redirect to `checkoutUrl` |\n| Fully custom payment UI | `2` then `3 · Headless Checkout` (PCI scope is yours) |\n| Bill a customer monthly | `4 · Clients` then `5 · Subscriptions` |\n| Sell from a hosted storefront | `6 · Products` |\n| Give money back | `8 · Refunds` |\n| Reconcile / export for finance | `9 · Transactions` |\n| Know when money moves | `11 · Webhooks` — **do this before going live** |\n\n---\n\n## Sandbox testing\n\nThe only PAN Chari's sandbox acquirer accepts is **`4918914107195005`**. Any\nother card number — including the classic `4242…` test PAN — is rejected upstream with a\ngeneric `502 BAAS_CHARI_ERROR`. Sandbox runs on real rails against a sandbox tenant:\nthe flows are genuine, the money is not.\n\nWebhook receivers must be **public HTTPS on port 443**. `localhost` and private\naddresses are rejected at registration, so use a tunnel during development.\n\n---\n\n## Also available\n\n- **Web reference & guides:** https://charipay.ma/fr/api-docs — generated from this same contract, with the OpenAPI spec and this collection as downloads\n",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "_exporter_id": "51803851",
    "_collection_link": "https://go.postman.co/collection/51803851-a4f4d07c-7c3d-498b-9f27-139babe3500e?source=collection_link"
  },
  "item": [
    {
      "name": "0 · Get Started (sandbox account → API key)",
      "item": [
        {
          "name": "1 · Sign up for a sandbox account",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('201 Created', () => pm.response.to.have.status(201));",
                  "const b = pm.response.json();",
                  "if (b.data && b.data.id) pm.collectionVariables.set('onboardingCaseId', b.data.id);",
                  "console.log('Now open the activation e-mail and paste its token into the activationToken variable.');"
                ]
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{signupEmail}}\",\n  \"firstName\": \"Amine\",\n  \"lastName\": \"Bennani\",\n  \"company\": \"Acme SARL\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/public/sandbox-signup",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "sandbox-signup"
              ]
            },
            "description": "Creates a self-serve sandbox onboarding case and e-mails you an activation link.\n\n**No authentication.** Public endpoint.\n\nBody fields (`SandboxSignupRequest`):\n- `email` *(required)* — must be a valid address; this is where the activation link is sent, and it becomes your portal login.\n- `firstName` *(required)* — your first name (≤ 255 chars).\n- `lastName` *(required)* — your last name (≤ 255 chars).\n- `company` — your company name (≤ 255 chars).\n- `type` — `MERCHANT` (default) or another `OnboardingCaseType`. Leave it out.\n\nReturns `201 Created` with the onboarding case (`id`, `state`, `email`, …). The\nAPI key is **not** created here and is **not** e-mailed — you create it yourself\nin step 7.\n\n**Next:** open the activation e-mail, copy the `token` query parameter out of\nthe link, and paste it into the `activationToken` collection variable."
          },
          "response": []
        },
        {
          "name": "2 · Activate — set your password",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('200 OK', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"token\": \"{{activationToken}}\",\n  \"password\": \"{{portalPassword}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/public/sandbox-signup/set-password",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "public",
                "sandbox-signup",
                "set-password"
              ]
            },
            "description": "Verifies your e-mail, activates the sandbox tenant and creates your portal login.\n\n**No authentication** — the one-time `token` from the activation e-mail *is* the\ncredential. Set `activationToken` before running this.\n\nBody fields (`SetPasswordRequest`):\n- `token` *(required)* — the one-time invitation token from the e-mail.\n- `password` *(required)* — 8–128 characters.\n\nThe token is single-use; replaying it fails."
          },
          "response": []
        },
        {
          "name": "3 · Log in to the portal",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "const d = b.data || {};",
                  "if (d.requiresDeviceVerification) { console.warn('New device: complete /api/v1/auth/device-verification/verify-otp first.'); }",
                  "if (d.requiresMfaChallenge || d.requiresMfaEnrollment) { console.warn('MFA required: enrol/verify TOTP in the portal, then rerun this request.'); }",
                  "if (d.requiresCompanySelection) {",
                  "  pm.collectionVariables.set('companySelectionToken', d.companySelectionToken);",
                  "  const c = (d.companies || [])[0] || {};",
                  "  if (c.enterpriseId) pm.collectionVariables.set('enterpriseId', c.enterpriseId);",
                  "  pm.test('Company-selection token captured', () => pm.expect(d.companySelectionToken).to.be.a('string'));",
                  "  console.log('Merchant account: request 4 (select company) is what returns the JWT.');",
                  "}",
                  "const auth = d.authResponse;",
                  "if (auth && auth.accessToken) {",
                  "  pm.collectionVariables.set('jwt', auth.accessToken);",
                  "  if (auth.refreshToken) pm.collectionVariables.set('refreshToken', auth.refreshToken);",
                  "  pm.test('JWT captured', () => pm.expect(auth.accessToken).to.be.a('string'));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Device-Id",
                "value": "{{deviceId}}",
                "description": "Stable fingerprint — keeps you off the new-device e-mail gate."
              },
              {
                "key": "X-Device-Name",
                "value": "Postman"
              },
              {
                "key": "X-Device-Type",
                "value": "WEB"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"username\": \"{{signupEmail}}\",\n  \"password\": \"{{portalPassword}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/auth/login",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "auth",
                "login"
              ]
            },
            "description": "Checks your credentials and captures the company-selection token — request 4 exchanges it for `{{jwt}}`.\n\n**No authentication.** Rate-limited per IP and per username.\n\nOptional device headers (`X-Device-Id`, `X-Device-Name`, `X-Device-Type`) are\nsent below — a stable `X-Device-Id` avoids re-triggering the new-device e-mail\nverification flow on every run.\n\nThe response is a `LoginResponse`, which is **not always** a token. Check the\nflags before reading `authResponse`:\n\n| Flag | What to do |\n|---|---|\n| `requiresDeviceVerification` | verify via `/api/v1/auth/device-verification/*` |\n| `requiresMfaEnrollment` / `requiresMfaChallenge` | complete `/api/v1/auth/mfa/*` |\n| `requiresCompanySelection` | call `POST /api/v1/auth/select-company` |\n| none set | `data.authResponse.accessToken` is your JWT |\n\nThe test script handles the plain case and warns on the others."
          },
          "response": []
        },
        {
          "name": "4 · Select your company — captures {{jwt}}",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "const d = b.data || {};",
                  "if (d.accessToken) {",
                  "  pm.collectionVariables.set('jwt', d.accessToken);",
                  "  if (d.refreshToken) pm.collectionVariables.set('refreshToken', d.refreshToken);",
                  "  pm.test('JWT captured', () => pm.expect(d.accessToken).to.be.a('string'));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-Device-Id",
                "value": "{{deviceId}}",
                "description": "Stable fingerprint — keeps you off the new-device e-mail gate."
              },
              {
                "key": "X-Device-Name",
                "value": "Postman"
              },
              {
                "key": "X-Device-Type",
                "value": "WEB"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"companySelectionToken\": \"{{companySelectionToken}}\",\n  \"enterpriseId\": \"{{enterpriseId}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/auth/select-company",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "auth",
                "select-company"
              ]
            },
            "description": "Completes the login for merchant accounts — which is every account created by\nrequest 1. The login itself never returns the JWT to a merchant: it answers\n`requiresCompanySelection` with a short-lived `companySelectionToken`, even\nwhen the account belongs to a single company. This request exchanges that\ntoken (plus the `enterpriseId` captured at step 3) for the session token pair,\nand stores `{{jwt}}`.\n\n**No authentication header** — the selection token in the body is the credential."
          },
          "response": []
        },
        {
          "name": "5 · Mint a step-up token",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "const t = (b.data && b.data.token) || b.token;",
                  "if (t) pm.collectionVariables.set('stepUpToken', t);"
                ]
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{jwt}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"action\": \"API_KEY_CREATE\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/auth/step-up",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "auth",
                "step-up"
              ]
            },
            "description": "Creating an API key is a sensitive action guarded by step-up re-authentication.\n\n**Auth: portal JWT.**\n\nSend the action you are about to perform; you get back a short-lived single-use\ntoken which the next request replays in the `X-StepUp-Token` header.\n\n- `action` *(required)* — here, `API_KEY_CREATE`.\n\nIf step-up is disabled in your environment this call is harmless and the header\nis simply ignored; if it is enabled and you skip it, step 7 returns\n`403 STEP_UP_REQUIRED`."
          },
          "response": []
        },
        {
          "name": "6 · List grantable permissions",
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{jwt}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/api-keys/available-permissions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "api-keys",
                "available-permissions"
              ]
            },
            "description": "Every permission code your enterprise is allowed to put on an API key.\n\n**Auth: portal JWT.** Requires `api_key:read`.\n\nThis is the intersection of the API-grantable catalog and your own enterprise\ngrant — every code returned is guaranteed to pass validation in step 7. Use it\ninstead of guessing scope names.\n\nRelated: `GET /api/v1/api-keys/configuration` returns your key limits,\nexpiration policy, rate limit and whether an IP whitelist is mandatory."
          },
          "response": []
        },
        {
          "name": "7 · Create your sandbox API key",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('201 Created', () => pm.response.to.have.status(201));",
                  "const b = pm.response.json();",
                  "if (b.apiKey) {",
                  "  pm.collectionVariables.set('apiKey', b.apiKey);",
                  "  pm.collectionVariables.set('apiKeyId', b.id);",
                  "  console.log('Sandbox key stored in {{apiKey}} — prefix', b.keyPrefix);",
                  "}",
                  "pm.test('is a sandbox key', () => pm.expect(String(b.apiKey || '')).to.include('chari_sk_test_'));"
                ]
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{jwt}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "X-StepUp-Token",
                "value": "{{stepUpToken}}",
                "description": "Single-use token from step 4; required when step-up is enabled."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Postman sandbox key\",\n  \"description\": \"Created from the Chari Pay Postman collection\",\n  \"environment\": \"SANDBOX\",\n  \"permissions\": [\n    \"payment_link:create\",\n    \"payment_link:read\",\n    \"payment_link:cancel\",\n    \"payment_session:read\",\n    \"payment_session:write\",\n    \"client:read\",\n    \"client:write\",\n    \"subscription:read\",\n    \"subscription:write\",\n    \"product:read\",\n    \"product:manage\",\n    \"order:read\",\n    \"balance:read\",\n    \"wallet:fund\",\n    \"refund:create\",\n    \"refund:read\",\n    \"transaction:read\",\n    \"transaction:export\",\n    \"webhook:read\",\n    \"webhook:manage\",\n    \"analytics:read\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/api-keys",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "api-keys"
              ]
            },
            "description": "Mints the key this whole collection runs on, and stores it in `{{apiKey}}`.\n\n**Auth: portal JWT** + `api_key:manage` + the step-up token from step 5.\n\nBody fields (`CreateApiKeyRequest`):\n- `name` *(required)* — label, ≤ 100 chars.\n- `description` — ≤ 500 chars.\n- `permissions` — the scope codes from step 6. The set below covers every folder\n  in this collection.\n- `environment` — `SANDBOX` or `PRODUCTION`. **Omit it and you get `SANDBOX`**\n  unless your enterprise is production-enabled. Be explicit.\n- `expiresAt` — ISO-8601 UTC. Whether it may be omitted depends on your\n  enterprise's `allowNonExpiringKeys` policy.\n- `rateLimit` — requests/minute override.\n- `allowedIps` — comma-separated IP whitelist (mandatory for some enterprises).\n- `associatedUserId` — create the key on behalf of another user (admins only).\n\n> ⚠️ **The full key is returned exactly once**, in `apiKey`. It is never\n> retrievable again — only its `keyPrefix` is. Store it in a secret manager.\n> The script below writes it into the collection variable `apiKey`, which is\n> marked *secret*.\n\nA sandbox key looks like `chari_sk_test_…`; production is `chari_sk_live_…`."
          },
          "response": []
        },
        {
          "name": "8 · Smoke test — read your balance",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('API key works', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/wallet",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "wallet"
              ]
            },
            "description": "Proves the key works. **Auth: API key** (`X-CHARI-PAY-API-KEY`), scope `balance:read`.\n\nA `200` here means you are fully set up: every other folder in this collection\nwill now run. A `401` means the key was not captured; a `403` means the key is\nmissing the `balance:read` scope.\n\nFrom here:\n- **Collect a one-off payment** → folder `1 · Payment Links`\n- **Embed checkout in your own app** → folders `2` and `3`\n- **Bill a customer on a schedule** → folders `4` and `5`\n- **Get notified of payments** → folder `11 · Webhooks`"
          },
          "response": []
        }
      ],
      "description": "# Start here — from nothing to your first API call\n\nRun these requests **in order** (or hit **Run folder**). You need no\ncredentials to begin: request 1 creates your sandbox account, request 7 mints\nyour sandbox API key and stores it in `{{apiKey}}`, and every other folder in\nthis collection then works immediately.\n\n| # | What happens | Auth |\n|---|--------------|------|\n| 1 | Self-serve sandbox signup — we e-mail you an activation link | none |\n| 2 | Set your password using the token from that e-mail | none |\n| 3 | Log in — captures `{{companySelectionToken}}` and `{{enterpriseId}}` | none |\n| 4 | Select your company — captures `{{jwt}}` | selection token |\n| 5 | Mint a step-up token — captures `{{stepUpToken}}` | JWT |\n| 6 | See which permissions your account may grant a key | JWT |\n| 7 | Create the sandbox API key — captures `{{apiKey}}` | JWT |\n| 8 | Smoke test: read your wallet balance with the new key | API key |\n\n**Why request 4 exists.** The login never hands the JWT to a merchant\naccount: it answers `requiresCompanySelection` with a short-lived\n`companySelectionToken` — even for a single-company account, which is exactly\nwhat request 1 creates. `POST /api/v1/auth/select-company` is the response\nthat carries `{{jwt}}`. (Depending on your account's permissions, the portal\nmay also require a TOTP enrolment at first login — complete it in the portal\nUI, then rerun request 3.)\n\n**Manual step between 1 and 2.** The activation token arrives by e-mail and is\nnever returned in an API response. Open the mail, copy the `token` out of the\nlink, and paste it into the `activationToken` collection variable.\n\n**Sandbox vs production.** The environment is decided by *the key itself*, not\nby the URL and not by a header:\n\n| Key prefix | Environment |\n|---|---|\n| `chari_sk_test_…` | Sandbox |\n| `chari_sk_live_…` | Production |\n\nA production key only works once your account is production-enabled; until then\nit returns `403 PRODUCTION_ACCESS_NOT_ENABLED`. Going live is a separate KYB flow —\nthis collection covers sandbox end-to-end.\n"
    },
    {
      "name": "1 · Payment Links",
      "item": [
        {
          "name": "Create payment link",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "const b = pm.response.json();",
                  "if (b.reference) pm.collectionVariables.set('linkReference', b.reference);",
                  "if (b.payUrl) { pm.collectionVariables.set('payUrl', b.payUrl); console.log('Open in a browser to pay:', b.payUrl); }",
                  "pm.test('created or replayed', () => pm.expect([200, 201]).to.include(pm.response.code));"
                ],
                "type": "text/javascript",
                "packages": {},
                "requests": {}
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 149.9,\n  \"description\": \"Order #1234\",\n  \"singleUse\": false,\n  \"paymentMethod\": \"CARD\",\n  \"customerName\": \"Amine Bennani\",\n  \"customerEmail\": \"buyer@example.com\",\n  \"customerPhone\": \"+212600000000\",\n  \"externalId\": \"order-{{$timestamp}}\",\n  \"metadata\": {\n    \"cartId\": \"c_987\",\n    \"source\": \"postman\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/payment-links",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "payment-links"
              ]
            },
            "description": "Creates a hosted payment page and returns its `payUrl` — send that to your buyer.\n\n**Scope:** `payment_link:create`.\n\nBody (`CreatePaymentLinkRequest`):\n- `amount` *(required)* — positive, **MAD major units** (`149.90`, not centimes).\n- `description` *(required)* — ≤ 255 chars, shown to the buyer.\n- `singleUse` — `true` (default) consumes the link after one successful payment; `false` makes it reusable.\n- `paymentMethod` — default shown on the hosted page; defaults to `CARD`.\n- `customerName` / `customerEmail` / `customerPhone` — optional buyer prefill; `customerPhone` is E.164.\n- `expiresAt` — ISO-8601 UTC, must be in the future.\n- `acceptUrl` / `declineUrl` — where the buyer lands after success / failure. **`https://` only.**\n- `notificationUrl` — per-link webhook target, **in addition to** your registered endpoints. `https://` only.\n- `externalId` — your own id, unique per merchant *per environment*. Re-creating with an existing `externalId` returns the **existing** link with `200` instead of a duplicate.\n- `metadata` — free-form object, echoed back on reads and in the payment webhook.\n\nSending an `Idempotency-Key` header (auto-generated by this collection) makes a\nnetwork retry safe. `externalId` and `Idempotency-Key` are independent\nmechanisms — use `Idempotency-Key` for retry safety, `externalId` when your\nsystem owns the identity.\n\nResponse `PaymentLinkResponse` — notable fields: `reference` (`pl_…`), `payUrl`,\n`status`, `useCount`, `totalCollected`, and `cashinCode`/`cashinEntity` when the\nlink is payable in cash at an agency."
          },
          "response": []
        },
        {
          "name": "List payment links",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/payment-links?page=0&size=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "payment-links"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "0",
                  "description": "Zero-based page index."
                },
                {
                  "key": "size",
                  "value": "20",
                  "description": "Page size."
                }
              ]
            },
            "description": "Paginated list, newest first. **Scope:** `payment_link:read`.\n\nStandard `page`/`size` paging; the response is a Spring `Page` (`content`, `totalElements`, `totalPages`, `number`, `size`)."
          },
          "response": []
        },
        {
          "name": "Retrieve payment link",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/payment-links/{{linkReference}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "payment-links",
                "{{linkReference}}"
              ]
            },
            "description": "Fetches one link by its `pl_…` reference. **Scope:** `payment_link:read`.\n\n`useCount` and `totalCollected` tell you how much a reusable link has collected so far. `404` if the reference does not belong to your account."
          },
          "response": []
        },
        {
          "name": "Cancel payment link",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/payment-links/{{linkReference}}/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "payment-links",
                "{{linkReference}}",
                "cancel"
              ]
            },
            "description": "Stops the link accepting further payments. **Scope:** `payment_link:cancel`.\n\nIdempotent in effect — the returned `status` becomes `CANCELLED`. Already-completed payments are untouched; refund those via folder `9 · Refunds`. Emits `payment_link.cancelled`."
          },
          "response": []
        },
        {
          "name": "Payment-link QR code (PNG)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/payment-links/{{linkReference}}/qr",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "payment-links",
                "{{linkReference}}",
                "qr"
              ]
            },
            "description": "Returns the link's QR code as `image/png`. **Scope:** `payment_link:read`.\n\nBinary response — use **Send and Download** in Postman to save it. Print it, or embed it in your own receipt."
          },
          "response": []
        },
        {
          "name": "Payment-link poster (PDF)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/payment-links/{{linkReference}}/poster",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "payment-links",
                "{{linkReference}}",
                "poster"
              ]
            },
            "description": "Returns a print-ready A4 poster (*affichette*) as `application/pdf`, QR included. **Scope:** `payment_link:read`.\n\nBinary response — use **Send and Download**."
          },
          "response": []
        },
        {
          "name": "E-mail the payment link",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"buyer@example.com\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/payment-links/{{linkReference}}/send",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "payment-links",
                "{{linkReference}}",
                "send"
              ]
            },
            "description": "Sends the link by e-mail. **Scope:** `payment_link:read`. Returns `202 Accepted`.\n\nThe body is optional (`SendPaymentLinkRequest`):\n- `email` — override recipient. Omit the body entirely to use the link's stored `customerEmail`."
          },
          "response": []
        }
      ],
      "description": "# Payment Links — `/v1/payment-links`\n\nThe fastest way to get paid: create a link, send the `payUrl` to your buyer, get\na `payment.succeeded` webhook when they pay. No frontend work.\n\n**Scopes:** `payment_link:create`, `payment_link:read`, `payment_link:cancel`.\n\nSingle-use links die after one successful payment; reusable links keep\ncollecting and track `useCount` / `totalCollected`. Run the requests in order —\n**Create** captures `{{linkReference}}` and `{{payUrl}}` for the rest.\n\n**Events emitted:** `payment_link.created`, `payment_link.updated`,\n`payment_link.expired`, `payment_link.cancelled`, and — when a buyer pays —\n`payment.succeeded` / `payment.failed`."
    },
    {
      "name": "2 · Checkout Sessions",
      "item": [
        {
          "name": "Create checkout session",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "const b = pm.response.json();",
                  "if (b.sessionId) pm.collectionVariables.set('sessionId', b.sessionId);",
                  "if (b.verifyKey) pm.collectionVariables.set('verifyKey', b.verifyKey);",
                  "if (b.checkoutUrl) { pm.collectionVariables.set('checkoutUrl', b.checkoutUrl); console.log('Hosted checkout:', b.checkoutUrl); }"
                ],
                "type": "text/javascript",
                "packages": {},
                "requests": {}
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 250.0,\n  \"orderId\": \"OR914-{{$timestamp}}\",\n  \"singleUse\": true,\n  \"externalId\": \"sess-{{$timestamp}}\",\n  \"notifyOnFailure\": true,\n  \"metadata\": {\n    \"source\": \"postman\"\n  },\n  \"config\": {\n    \"customer\": {\n      \"email\": \"buyer@example.com\",\n      \"phone\": \"+212600000000\",\n      \"firstName\": \"Amine\",\n      \"lastName\": \"Bennani\"\n    },\n    \"urls\": {\n      \"accept\": \"https://example.com/success\",\n      \"decline\": \"https://example.com/cancel\",\n      \"notification\": \"https://example.com/webhooks/charipay\",\n      \"externalReference\": \"ORD-20269-0005\"\n    },\n    \"keepAlive\": false\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/payment-sessions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "payment-sessions"
              ]
            },
            "description": "Creates a one-shot hosted checkout and returns `checkoutUrl` + a single-use `verifyKey`.\n\n**Scope:** `payment_session:write`.\n\nBody (`CreateCheckoutSessionRequest`):\n- `amount` *(required)* — MAD major units.\n- `orderId` *(required)* — your order reference for this checkout.\n- `singleUse` — defaults to `true`.\n- `externalId` — unique per merchant; a duplicate **replays the existing session**. Distinct from `orderId`, which is not uniqueness-enforced.\n- `expiresAt` — must be in the future; defaults to 72 h after creation.\n- `notifyOnFailure` — also send `payment.failed`. Defaults to `false`.\n- `metadata` — ≤ 4 KB, echoed in the payment webhook.\n- `config` *(required)*:\n  - `customer` *(required)* — `email`, `phone` (E.164), `firstName`, `lastName` all required; `phoneClient` optional.\n  - `urls` — `accept`, `decline`, `notification` (all `https://`, all optional, falling back to your merchant default then the platform default) and `externalReference` (echoed as `ExternalId` in webhooks; defaults to `orderId`).\n  - `keepAlive` — keep the session usable after a failed attempt so the buyer can retry. **Also the flag that makes the hosted page ask for consent to store the card** — needed for saved payment methods and subscriptions.\n  - `frontend` — `theme`, `logo`, `companyName` branding for the hosted page.\n  - `settlement` — `walletId` / `subMerchantReference` to credit a sub-merchant instead of your own wallet.\n\nResponse `StorePaymentDataResponse`: `sessionId` (`ps_…`), `verifyKey` (the\nsingle-use `vk`), `checkoutUrl`, `expiresAt`, and a server-side `sessionToken`.\n\n**Two ways to finish the payment:** redirect the buyer to `checkoutUrl` (hosted),\nor drive it yourself from folder `3 · Headless Checkout` using `sessionId` + `vk`."
          },
          "response": [
            {
              "name": "Create checkout session",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"amount\": 250.0,\n  \"orderId\": \"OR914-{{$timestamp}}\",\n  \"singleUse\": true,\n  \"externalId\": \"sess-{{$timestamp}}\",\n  \"notifyOnFailure\": true,\n  \"metadata\": {\n    \"source\": \"postman\"\n  },\n  \"config\": {\n    \"customer\": {\n      \"email\": \"buyer@example.com\",\n      \"phone\": \"+212600000000\",\n      \"firstName\": \"Amine\",\n      \"lastName\": \"Bennani\"\n    },\n    \"urls\": {\n      \"accept\": \"https://example.com/success\",\n      \"decline\": \"https://example.com/cancel\",\n      \"notification\": \"https://example.com/webhooks/charipay\",\n      \"externalReference\": \"ORD-20269-0005\"\n    },\n    \"keepAlive\": false\n  }\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/v1/payment-sessions",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "payment-sessions"
                  ]
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": null,
              "header": [
                {
                  "key": ":status",
                  "value": 201
                },
                {
                  "key": "server",
                  "value": "nginx/1.24.0 (Ubuntu)"
                },
                {
                  "key": "date",
                  "value": "Thu, 20 Aug 2026 19:43:17 GMT"
                },
                {
                  "key": "content-type",
                  "value": "application/json"
                },
                {
                  "key": "x-request-id",
                  "value": "req_9d75471a-b0ef-4f2c-9c74-77d43dea213b"
                },
                {
                  "key": "x-content-type-options",
                  "value": "nosniff"
                },
                {
                  "key": "x-xss-protection",
                  "value": "0"
                },
                {
                  "key": "cache-control",
                  "value": "no-cache, no-store, max-age=0, must-revalidate"
                },
                {
                  "key": "pragma",
                  "value": "no-cache"
                },
                {
                  "key": "expires",
                  "value": "0"
                },
                {
                  "key": "x-frame-options",
                  "value": "DENY"
                }
              ],
              "cookie": [],
              "body": "{\n    \"sessionId\": \"SES-fde2f3355\",\n    \"sessionToken\": \"tok_1e64b9c126938dbcf582422045c1d879\",\n    \"verifyKey\": \"vk_5f8d1cec5d4ec206958dd029\",\n    \"expiresAt\": \"2026-08-23T19:43:16.908862034Z\",\n    \"checkoutUrl\": \"https://psp.charipay.ma/checkout?sessionId=SES-fde2f3355&vk=vk_5f8d1cec5d4ec206958dd029\"\n}"
            }
          ]
        },
        {
          "name": "List checkout sessions",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/payment-sessions?page=0&size=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "payment-sessions"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "0"
                },
                {
                  "key": "size",
                  "value": "20"
                },
                {
                  "key": "origin",
                  "value": "API",
                  "description": "API or DASHBOARD.",
                  "disabled": true
                }
              ]
            },
            "description": "Paginated list of your sessions. **Scope:** `payment_session:read`.\n\n- `origin` — filter by how the session was created: `API` (this collection) or `DASHBOARD`."
          },
          "response": []
        },
        {
          "name": "Retrieve checkout session",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/payment-sessions/{{sessionId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "payment-sessions",
                "{{sessionId}}"
              ]
            },
            "description": "Current status of one session. **Scope:** `payment_session:read`.\n\nUse this to poll while the buyer is on the hosted page — but treat the **webhook** as the source of truth for money movement, not polling."
          },
          "response": []
        },
        {
          "name": "Cancel checkout session",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/payment-sessions/{{sessionId}}/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "payment-sessions",
                "{{sessionId}}",
                "cancel"
              ]
            },
            "description": "Invalidates the session so it can no longer be paid. **Scope:** `payment_session:write`."
          },
          "response": []
        }
      ],
      "description": "# Checkout Sessions — `/v1/payment-sessions`\n\nA one-shot, per-order hosted checkout. Use this instead of a payment link when\nthe amount and buyer are decided by your own cart at request time.\n\n**Scopes:** `payment_session:write`, `payment_session:read`.\n\n**Create** captures `{{sessionId}}`, `{{verifyKey}}` and `{{checkoutUrl}}` — the\nlast two feed folder `3 · Headless Checkout`.\n\n**Link or session?** A payment link is a durable, shareable URL you can print on\nan invoice. A session is created per order, expires (72 h by default) and is\nusually single-use."
    },
    {
      "name": "3 · Headless Checkout (no API key)",
      "item": [
        {
          "name": "Verify — open the session",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sessionId\": \"{{sessionId}}\",\n  \"vk\": \"{{verifyKey}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/checkout/verify",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "checkout",
                "verify"
              ]
            },
            "description": "Opens a session for payment and returns what the page should render.\n\n**No API key.** The `sessionId` + `vk` pair *is* the credential — the `vk` is\nsingle-use on single-use sessions, so calling verify twice on one will fail.\n\nBody (`CheckoutVerifyRequest`):\n- `sessionId` *(required)* — from **Create checkout session**.\n- `vk` *(required)* — the `verifyKey` from the same response.\n\nThe response tells you the amount, the buyer, and `keepAlive` — when `keepAlive`\nis `true` your page **must** collect explicit consent before submitting, because\nthe card will be stored."
          },
          "response": [
            {
              "name": "Verify — open the session",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"sessionId\": \"{{sessionId}}\",\n  \"vk\": \"{{verifyKey}}\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/checkout/verify",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "checkout",
                    "verify"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": null,
              "header": [
                {
                  "key": ":status",
                  "value": 200
                },
                {
                  "key": "server",
                  "value": "nginx/1.24.0 (Ubuntu)"
                },
                {
                  "key": "date",
                  "value": "Thu, 20 Aug 2026 19:43:23 GMT"
                },
                {
                  "key": "content-type",
                  "value": "application/json"
                },
                {
                  "key": "x-request-id",
                  "value": "req_abbea608-ad27-4632-a6a5-7f82bcdfcd23"
                },
                {
                  "key": "x-content-type-options",
                  "value": "nosniff"
                },
                {
                  "key": "x-xss-protection",
                  "value": "0"
                },
                {
                  "key": "cache-control",
                  "value": "no-cache, no-store, max-age=0, must-revalidate"
                },
                {
                  "key": "pragma",
                  "value": "no-cache"
                },
                {
                  "key": "expires",
                  "value": "0"
                },
                {
                  "key": "x-frame-options",
                  "value": "DENY"
                }
              ],
              "cookie": [],
              "body": "{\n    \"sessionId\": \"SES-fde2f3355\",\n    \"status\": \"ACTIVE\",\n    \"amount\": 250,\n    \"currency\": \"MAD\",\n    \"orderId\": \"OR914-1787254994\",\n    \"keepAlive\": false,\n    \"branding\": {\n        \"displayName\": \"Chari Pay\",\n        \"primaryColor\": \"#E86C4A\",\n        \"theme\": \"default\",\n        \"locale\": \"fr\"\n    },\n    \"expiresAt\": \"2026-08-23T19:43:16.908862Z\",\n    \"testMode\": true,\n    \"defaultPaymentMethod\": \"CARD\",\n    \"cashAvailable\": false\n}"
            }
          ]
        },
        {
          "name": "Submit — capture the card",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (b.operationId) pm.collectionVariables.set('operationId', b.operationId);"
                ]
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sessionId\": \"{{sessionId}}\",\n  \"savePaymentMethodConsent\": false,\n  \"card\": {\n    \"firstName\": \"Amine\",\n    \"lastName\": \"Bennani\",\n    \"pan\": \"4918914107195005\",\n    \"expiryDate\": \"12/27\",\n    \"cvv\": \"123\",\n    \"cardName\": \"AMINE BENNANI\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/checkout/submit",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "checkout",
                "submit"
              ]
            },
            "description": "Submits card details for an already-verified session and starts 3-D Secure.\n\n**No API key** — session-scoped.\n\nBody (`CheckoutSubmitRequest`):\n- `sessionId` *(required)*.\n- `card` *(required)* — `firstName`, `lastName`, `pan`, `expiryDate` (`MM/YY`), `cvv` all required; `cardName` optional.\n- `savePaymentMethodConsent` — **required `true`** when verify returned `keepAlive: true`; ignored otherwise.\n\n> 🔒 The PAN is never stored unmasked and is excluded from logs. If you build\n> your own page you are taking on PCI scope — the hosted `checkoutUrl` avoids\n> that entirely.\n\n**Sandbox test card:** `4918914107195005` is the only PAN Chari's sandbox\nacquirer accepts. Any other number (including the classic `4242…` test PAN) is rejected\nupstream with a generic `502 BAAS_CHARI_ERROR`.\n\nThe response carries the 3DS redirect. Send the buyer there, then confirm with\n**Return**."
          },
          "response": [
            {
              "name": "Submit — capture the card",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"sessionId\": \"{{sessionId}}\",\n  \"savePaymentMethodConsent\": false,\n  \"card\": {\n    \"firstName\": \"Amine\",\n    \"lastName\": \"Bennani\",\n    \"pan\": \"4918914107195005\",\n    \"expiryDate\": \"12/27\",\n    \"cvv\": \"123\",\n    \"cardName\": \"AMINE BENNANI\"\n  }\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                },
                "url": {
                  "raw": "{{baseUrl}}/checkout/submit",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "checkout",
                    "submit"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": null,
              "header": [
                {
                  "key": ":status",
                  "value": 200
                },
                {
                  "key": "server",
                  "value": "nginx/1.24.0 (Ubuntu)"
                },
                {
                  "key": "date",
                  "value": "Thu, 20 Aug 2026 19:43:32 GMT"
                },
                {
                  "key": "content-type",
                  "value": "application/json"
                },
                {
                  "key": "x-request-id",
                  "value": "req_f9ca2b70-2b9d-45f6-aef4-e8bb63933d9a"
                },
                {
                  "key": "x-content-type-options",
                  "value": "nosniff"
                },
                {
                  "key": "x-xss-protection",
                  "value": "0"
                },
                {
                  "key": "cache-control",
                  "value": "no-cache, no-store, max-age=0, must-revalidate"
                },
                {
                  "key": "pragma",
                  "value": "no-cache"
                },
                {
                  "key": "expires",
                  "value": "0"
                },
                {
                  "key": "x-frame-options",
                  "value": "DENY"
                }
              ],
              "cookie": [],
              "body": "{\n    \"operationId\": 1,\n    \"reference\": \"ORD-20269-0005\",\n    \"status\": \"PENDING_3DS\",\n    \"redirectionUrl\": \"https://staging-api.charipay.ma:443/chari-frontend/home_card3?ORDER_ID=CHf5cc246d8e0e&REFERENCE_ID=7720&TRACK_ID=060186865258\"\n}"
            }
          ]
        },
        {
          "name": "Return — confirm after 3DS",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"sessionId\": \"{{sessionId}}\",\n  \"operationId\": \"{{operationId}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/checkout/return",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "checkout",
                "return"
              ]
            },
            "description": "Correlates the buyer's return from the 3-D Secure challenge with the operation.\n\n**No API key.**\n\nBody (`CheckoutReturnRequest`):\n- `sessionId` *(required)*.\n- `operationId` *(required)* — the numeric id from **Submit**."
          },
          "response": []
        },
        {
          "name": "Public payment status",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/checkout/payments/{{paymentReference}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "checkout",
                "payments",
                "{{paymentReference}}"
              ]
            },
            "description": "Public status of one payment by its reference (e.g. `ord_7Hq2Lm`). **No API key.**\n\nSafe to call from a buyer-facing “thank you” page — it exposes only public status fields."
          },
          "response": []
        }
      ],
      "description": "# Headless checkout — `/checkout/*`\n\nDrive the payment from **your own** UI instead of redirecting to the hosted page.\nOrder: **verify → submit → 3DS → return**.\n\n> ⚠️ **Never send your API key to these endpoints.** They are *session-scoped*:\n> authorisation comes from the session's single-use verify key (`vk`), which is\n> safe to hand to a browser. Your API key is not, and this collection therefore\n> sets **No Auth** on every request in this folder.\n\nSkip this folder entirely if you redirect buyers to `checkoutUrl` — the hosted\npage does all of it for you, and keeps card data out of your PCI scope.\n\n**Sandbox test card:** `4918914107195005`."
    },
    {
      "name": "4 · Clients",
      "item": [
        {
          "name": "Create client",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (b.id) pm.collectionVariables.set('clientId', b.id);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Amine Bennani\",\n  \"email\": \"amine@example.com\",\n  \"phone\": \"+212600000000\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/clients",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "clients"
              ]
            },
            "description": "Creates a reusable customer record. **Scope:** `client:write`.\n\nBody (`CreateClientRequest`):\n- `name` *(required)* — ≤ 255 chars.\n- `email` — valid address, ≤ 255 chars.\n- `phone` — E.164, ≤ 30 chars.\n\nReturns a `ClientResponse` whose `id` is a **UUID** (not a `cli_…` reference).\nThat UUID is what `POST /v1/subscriptions` expects as `clientId`."
          },
          "response": []
        },
        {
          "name": "List clients",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/clients?page=0&size=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "clients"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "0"
                },
                {
                  "key": "size",
                  "value": "20"
                },
                {
                  "key": "origin",
                  "value": "API",
                  "description": "API or DASHBOARD.",
                  "disabled": true
                }
              ]
            },
            "description": "Paginated list. **Scope:** `client:read`.\n\n- `origin` — `API` or `DASHBOARD`."
          },
          "response": []
        },
        {
          "name": "Retrieve client",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/clients/{{clientId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "clients",
                "{{clientId}}"
              ]
            },
            "description": "One client by UUID. **Scope:** `client:read`."
          },
          "response": []
        },
        {
          "name": "Update client",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Amine Bennani\",\n  \"email\": \"amine.new@example.com\",\n  \"phone\": \"+212600000001\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/clients/{{clientId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "clients",
                "{{clientId}}"
              ]
            },
            "description": "Partial update — **only the fields you send are changed**, despite the `PUT`. **Scope:** `client:write`.\n\nAll three fields (`name`, `email`, `phone`) are optional."
          },
          "response": []
        },
        {
          "name": "List saved payment methods",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (Array.isArray(b) && b.length && b[0].id) pm.collectionVariables.set('paymentMethodId', b[0].id);"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/clients/{{clientId}}/payment-methods",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "clients",
                "{{clientId}}",
                "payment-methods"
              ]
            },
            "description": "Cards this client has consented to store. **Scope:** `client:read`.\n\nA payment method only appears here after a checkout that ran with\n`config.keepAlive: true` **and** `savePaymentMethodConsent: true`. These are the\nUUIDs you pass to `PUT /v1/subscriptions/{reference}/payment-method`."
          },
          "response": []
        },
        {
          "name": "Set default payment method",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/clients/{{clientId}}/payment-methods/{{paymentMethodId}}/default",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "clients",
                "{{clientId}}",
                "payment-methods",
                "{{paymentMethodId}}",
                "default"
              ]
            },
            "description": "Marks one stored card as the client's default. **Scope:** `client:write`.\n\nNew subscriptions with no explicit `paymentMethodId` reuse this one."
          },
          "response": []
        },
        {
          "name": "Delete payment method",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/clients/{{clientId}}/payment-methods/{{paymentMethodId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "clients",
                "{{clientId}}",
                "payment-methods",
                "{{paymentMethodId}}"
              ]
            },
            "description": "Forgets a stored card. **Scope:** `client:write`.\n\nSubscriptions still bound to it will fall back to capturing a new method at the next charge."
          },
          "response": []
        },
        {
          "name": "Delete client",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/clients/{{clientId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "clients",
                "{{clientId}}"
              ]
            },
            "description": "Deletes the client record. **Scope:** `client:write`. Returns `204 No Content`."
          },
          "response": []
        }
      ],
      "description": "# Clients — `/v1/clients`\n\nReusable customer records. You need one before you can create a subscription,\nand they are where saved cards live.\n\n**Scopes:** `client:read`, `client:write`.\n\n**Create** captures `{{clientId}}`; **List saved payment methods** captures\n`{{paymentMethodId}}`.\n\nNote the id shape: clients are identified by **UUID**, unlike links (`pl_…`),\nsessions (`ps_…`), subscriptions (`sub_…`) and products (`prod_…`)."
    },
    {
      "name": "5 · Subscriptions",
      "item": [
        {
          "name": "Create subscription",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (b.reference) pm.collectionVariables.set('subscriptionRef', b.reference);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"clientId\": \"{{clientId}}\",\n  \"amount\": 99.0,\n  \"description\": \"Gold plan — monthly\",\n  \"frequency\": \"MONTHLY\",\n  \"startDate\": \"2026-09-01\",\n  \"channels\": [\n    \"EMAIL\"\n  ],\n  \"autoPay\": true,\n  \"billingTime\": \"09:00\",\n  \"reminderDaysBefore\": 3,\n  \"externalId\": \"sub-{{$timestamp}}\",\n  \"metadata\": {\n    \"customerId\": \"cus_1001\",\n    \"plan\": \"gold\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/subscriptions",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "subscriptions"
              ]
            },
            "description": "Starts recurring billing for an existing client. **Scope:** `subscription:write`.\n\nBody (`CreateSubscriptionRequest`):\n- `clientId` *(required)* — UUID from folder `4`.\n- `amount` *(required)* — positive, MAD major units, charged each period.\n- `description` *(required)* — ≤ 255 chars, shown on every generated charge.\n- `frequency` *(required)* — e.g. `MONTHLY`.\n- `startDate` *(required)* — `YYYY-MM-DD`, first billing period.\n- `endDate` — open-ended when omitted.\n- `channels` *(required, non-empty)* — how the client is notified, e.g. `[\"EMAIL\"]`.\n- `autoPay` — defaults to `true`: store the card after the first 3DS payment and collect future periods automatically. Set `false` to e-mail a payment link each period instead.\n- `billingTime` — `HH:mm` in **Africa/Casablanca**; defaults to `09:00`.\n- `reminderDaysBefore` — 0–30, defaults to `3`.\n- `paymentMethodId` — a specific stored card. When omitted, the client's default is reused; if there is none, the first charge captures a new one.\n- `externalId` — unique per merchant and environment; a duplicate returns the **existing** subscription. Echoed as `ExternalId` in every subscription webhook.\n- `metadata` — ≤ 4 KB, echoed unchanged in every subscription webhook. Use opaque ids; **never** card data, CVV or credentials.\n\n**Events:** `subscription.payment_succeeded`, `subscription.payment_failed`,\n`subscription.canceled`."
          },
          "response": []
        },
        {
          "name": "List subscriptions",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/subscriptions?page=0&size=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "subscriptions"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "0"
                },
                {
                  "key": "size",
                  "value": "20"
                },
                {
                  "key": "origin",
                  "value": "API",
                  "description": "API or DASHBOARD.",
                  "disabled": true
                }
              ]
            },
            "description": "Paginated list. **Scope:** `subscription:read`.\n\n- `origin` — `API` or `DASHBOARD`."
          },
          "response": []
        },
        {
          "name": "Retrieve subscription",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/subscriptions/{{subscriptionRef}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "subscriptions",
                "{{subscriptionRef}}"
              ]
            },
            "description": "One subscription by its `sub_…` reference. **Scope:** `subscription:read`."
          },
          "response": []
        },
        {
          "name": "List subscription charges",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/subscriptions/{{subscriptionRef}}/charges",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "subscriptions",
                "{{subscriptionRef}}",
                "charges"
              ]
            },
            "description": "Every period generated so far, as payment links. **Scope:** `subscription:read`.\n\nReturns a plain array of `PaymentLinkResponse` — one per billing period, each with its own `status` and `payUrl`. This is the billing history."
          },
          "response": []
        },
        {
          "name": "Select payment method",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"paymentMethodId\": \"{{paymentMethodId}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/subscriptions/{{subscriptionRef}}/payment-method",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "subscriptions",
                "{{subscriptionRef}}",
                "payment-method"
              ]
            },
            "description": "Binds the subscription to a specific stored card. **Scope:** `subscription:write`.\n\n- `paymentMethodId` *(required)* — a **client-owned** payment-method UUID from folder `4`.\n\nUse this when a client's card is replaced without cancelling the subscription."
          },
          "response": []
        },
        {
          "name": "Pause subscription",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/subscriptions/{{subscriptionRef}}/pause",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "subscriptions",
                "{{subscriptionRef}}",
                "pause"
              ]
            },
            "description": "Suspends future charges without losing history. **Scope:** `subscription:write`."
          },
          "response": []
        },
        {
          "name": "Resume subscription",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/subscriptions/{{subscriptionRef}}/resume",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "subscriptions",
                "{{subscriptionRef}}",
                "resume"
              ]
            },
            "description": "Restarts a paused subscription. **Scope:** `subscription:write`.\n\nPeriods that elapsed while paused are not retro-charged."
          },
          "response": []
        },
        {
          "name": "Cancel subscription",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/subscriptions/{{subscriptionRef}}/cancel",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "subscriptions",
                "{{subscriptionRef}}",
                "cancel"
              ]
            },
            "description": "Ends the subscription permanently. **Scope:** `subscription:write`. Emits `subscription.canceled`.\n\nCancelling is terminal — to bill the client again, create a new subscription."
          },
          "response": []
        },
        {
          "name": "Test the automatic charge (sandbox)",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/subscriptions/{{subscriptionRef}}/test-auto-pay",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "subscriptions",
                "{{subscriptionRef}}",
                "test-auto-pay"
              ]
            },
            "description": "Forces one automatic collection immediately instead of waiting for the next period.\n\n**Scope:** `subscription:write`. Accepts an `Idempotency-Key`.\n\nBuilt for sandbox integration testing: it exercises the stored-card charge path\nend-to-end so you can see `subscription.payment_succeeded` (or `…_failed`) land\non your webhook endpoint without waiting a month.\n\nRequires `autoPay: true` and a stored payment method — i.e. the first period\nmust already have been paid with `keepAlive` + consent."
          },
          "response": []
        }
      ],
      "description": "# Subscriptions — `/v1/subscriptions`\n\nRecurring billing on top of a client record. With `autoPay: true` (the default),\nthe first period is paid with 3-D Secure, the card is stored with the buyer's\nconsent, and every later period is collected automatically.\n\n**Scopes:** `subscription:read`, `subscription:write`.\n\n**Prerequisite:** run folder `4 · Clients` first — `{{clientId}}` must be set.\n\n**The full happy path**\n1. Create a client (folder 4).\n2. Create the subscription here.\n3. The first charge appears under **List subscription charges** as a payment\n   link; the client pays it with 3DS. Consent stores the card.\n4. Every later period is charged automatically at `billingTime`\n   (Africa/Casablanca), with a reminder `reminderDaysBefore` days ahead.\n5. **Test the automatic charge** forces step 4 immediately in sandbox."
    },
    {
      "name": "6 · Products",
      "item": [
        {
          "name": "Create product",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (b.reference) pm.collectionVariables.set('productReference', b.reference);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Chari T-shirt\",\n  \"description\": \"100% cotton, unisex.\",\n  \"imageUrl\": \"https://cdn.example.com/tshirt.png\",\n  \"price\": 199.0,\n  \"currency\": \"MAD\",\n  \"stockQuantity\": 100,\n  \"externalId\": \"sku-{{$timestamp}}\",\n  \"metadata\": {\n    \"warehouse\": \"CASA-1\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/products",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "products"
              ]
            },
            "description": "Adds a product to your hosted storefront catalog. **Scope:** `product:manage`.\n\nBody (`CreateProductRequest`):\n- `name` *(required)* — ≤ 255 chars.\n- `price` *(required)* — positive, MAD major units.\n- `description` — ≤ 2000 chars.\n- `imageUrl` — ≤ 2000 chars.\n- `currency` — ISO-4217; defaults to `MAD`.\n- `stockQuantity` — ≥ 0; unlimited when omitted.\n- `expiresAt` — sale-ends timestamp, must be in the future.\n- `externalId` — your SKU, unique per merchant. A duplicate returns the **existing** product (idempotent replay).\n- `metadata` — echoed on reads and in the payment webhook of orders for this product.\n\nBuyers pay through the hosted product page; a paid order emits `order.paid`."
          },
          "response": []
        },
        {
          "name": "List products",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/products?page=0&size=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "products"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "0"
                },
                {
                  "key": "size",
                  "value": "20"
                },
                {
                  "key": "origin",
                  "value": "API",
                  "description": "API or DASHBOARD.",
                  "disabled": true
                }
              ]
            },
            "description": "Paginated catalog. **Scope:** `product:read`.\n\n- `origin` — `API` or `DASHBOARD`."
          },
          "response": []
        },
        {
          "name": "Retrieve product",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/products/{{productReference}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "products",
                "{{productReference}}"
              ]
            },
            "description": "One product by its `prod_…` reference. **Scope:** `product:read`."
          },
          "response": []
        },
        {
          "name": "Update product",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"price\": 149.9,\n  \"stockQuantity\": 80,\n  \"active\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/products/{{productReference}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "products",
                "{{productReference}}"
              ]
            },
            "description": "True partial update — only the fields present are changed. **Scope:** `product:manage`.\n\nOptional fields: `name`, `description`, `imageUrl`, `price`, `active`,\n`stockQuantity`, `expiresAt`, and `clearExpiration` (send `true` to remove an\nexisting expiry — you cannot clear it by sending `expiresAt: null`, because an\nabsent field means *leave unchanged*)."
          },
          "response": []
        },
        {
          "name": "Deactivate product",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/products/{{productReference}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "products",
                "{{productReference}}"
              ]
            },
            "description": "Takes the product off the storefront. **Scope:** `product:manage`.\n\nA **soft** delete — the product is deactivated, not erased, so existing orders keep resolving. Returns the updated product."
          },
          "response": []
        },
        {
          "name": "List product orders",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/products/{{productReference}}/orders?page=0&size=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "products",
                "{{productReference}}",
                "orders"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "0"
                },
                {
                  "key": "size",
                  "value": "20"
                }
              ]
            },
            "description": "Orders placed for this product. **Scope:** `order:read` (not `product:read`)."
          },
          "response": []
        }
      ],
      "description": "# Products — `/v1/products`\n\nA hosted storefront catalog: publish a product, share its page, and buyers pay by\ncard or cash-in without you writing any checkout code.\n\n**Scopes:** `product:read`, `product:manage`, plus `order:read` for the orders\nendpoint.\n\n**Create** captures `{{productReference}}`. Paid orders emit `order.paid`."
    },
    {
      "name": "7 · Wallet",
      "item": [
        {
          "name": "Retrieve balance",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/wallet",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "wallet"
              ]
            },
            "description": "Your merchant wallet balance. **Scope:** `balance:read`.\n\nThis is also the cheapest way to check that an API key is alive and correctly scoped."
          },
          "response": []
        },
        {
          "name": "Retrieve account (RIB)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/wallet/account",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "wallet",
                "account"
              ]
            },
            "description": "Your wallet's payment-account details, including the RIB. **Scope:** `balance:read`.\n\nSensitive: RIB exposure is access-controlled and audited. Do not log or cache the response."
          },
          "response": []
        },
        {
          "name": "Download RIB (PDF)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/wallet/account/rib-document",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "wallet",
                "account",
                "rib-document"
              ]
            },
            "description": "The RIB as a generated `application/pdf` attachment. **Scope:** `balance:read`.\n\nBinary response — use **Send and Download**."
          },
          "response": []
        },
        {
          "name": "Fund the wallet (cash-in)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Idempotency-Key",
                "value": "{{$guid}}",
                "description": "REQUIRED on this endpoint."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 250.0\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/wallet/cash-ins",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "wallet",
                "cash-ins"
              ]
            },
            "description": "Starts a wallet funding operation. **Scope:** `wallet:fund`.\n\n- `amount` *(required)* — ≥ 0.01 MAD, at most 2 decimals.\n\n> **`Idempotency-Key` is mandatory here** — unlike everywhere else, where it is\n> optional. This collection generates one automatically per send, so a re-send\n> creates a *new* cash-in; pin the header manually if you mean to replay.\n\nThe linked funding payment emits `payment.succeeded` on completion."
          },
          "response": []
        }
      ],
      "description": "# Wallet — `/v1/wallet`\n\nYour merchant balance and the payment account behind it.\n\n**Scopes:** `balance:read`; `wallet:fund` for cash-ins.\n\nCollected payments land in this wallet; settlements and bank transfers move the\nmoney out. `GET /v1/wallet` doubles as the smoke test for a new API key."
    },
    {
      "name": "8 · Refunds",
      "item": [
        {
          "name": "Create refund",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (b.reference) pm.collectionVariables.set('refundReference', b.reference);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"operationId\": \"{{operationId}}\",\n  \"refundReference\": \"rf-{{$timestamp}}\",\n  \"refundAmount\": 50.0,\n  \"reason\": \"Customer request\",\n  \"note\": \"Partial refund — one item returned\",\n  \"metadata\": {\n    \"ticket\": \"SUP-4412\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/refunds",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "refunds"
              ]
            },
            "description": "Refunds a card payment, fully or partially. **Scope:** `refund:create`.\n\nBody (`RefundPaymentRequest`) — identify the original payment by **either**:\n- `operationId` — the numeric `OperationId` you received in the payment webhook, **or**\n- `externalId` — the merchant `externalReference` of the original payment.\n\nPlus:\n- `refundReference` *(required)* — **your own** merchant-unique deterministic string, ≤ 64 chars. **This is the idempotency key**: replaying the same value returns the original refund instead of refunding twice. Derive it from your order id, do not use a random GUID.\n- `reason` *(required)*.\n- `refundAmount` — defaults to the **full remaining refundable balance**. Send a smaller number for a partial refund.\n- `note` — ≤ 128 chars.\n- `metadata` — free-form object.\n\n**Events:** `refund.succeeded` or `refund.failed`.\n\n> Card sales taken on a *merchant card terminal* refund through a different\n> endpoint (`/cards/merchant-payment/refund`), not this one."
          },
          "response": []
        },
        {
          "name": "List refunds",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/refunds?page=0&size=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "refunds"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "0"
                },
                {
                  "key": "size",
                  "value": "20"
                }
              ]
            },
            "description": "Paginated list of your refunds. **Scope:** `refund:read`."
          },
          "response": []
        },
        {
          "name": "Retrieve refund",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/refunds/{{refundReference}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "refunds",
                "{{refundReference}}"
              ]
            },
            "description": "One refund by reference. **Scope:** `refund:read`.\n\nPoll `status` here, or wait for `refund.succeeded` / `refund.failed` on your webhook endpoint — the webhook is authoritative."
          },
          "response": []
        }
      ],
      "description": "# Refunds — `/v1/refunds`\n\nGive money back, in full or in part.\n\n**Scopes:** `refund:create`, `refund:read`.\n\nThe mechanism that makes refunds safe is **`refundReference`** — a deterministic\nstring *you* choose. Two calls with the same `refundReference` produce one\nrefund, no matter how many times your retry logic fires. Compute it from your\norder id; never randomise it.\n\nRun folder `3` (or pay a link from folder `1`) first so `{{operationId}}` is\nset."
    },
    {
      "name": "9 · Transactions",
      "item": [
        {
          "name": "List transactions",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "const rows = b.content || b.data || [];",
                  "if (rows.length && rows[0].operationId) pm.collectionVariables.set('operationId', rows[0].operationId);"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/transactions?status=SUCCESS&type=PAYMENT&from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z&limit=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "transactions"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "SUCCESS",
                  "description": "PENDING, PENDING_3DS, SUCCESS, FAILED, CANCELED."
                },
                {
                  "key": "type",
                  "value": "PAYMENT",
                  "description": "PAYMENT, REFUND, BILL_PAYMENT, BANK_TRANSFER."
                },
                {
                  "key": "from",
                  "value": "2026-08-01T00:00:00Z",
                  "description": "ISO-8601 UTC."
                },
                {
                  "key": "to",
                  "value": "2026-08-31T23:59:59Z",
                  "description": "ISO-8601 UTC."
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Page size for cursor paging."
                },
                {
                  "key": "method",
                  "value": "CARD",
                  "description": "CARD, CASHIN…",
                  "disabled": true
                },
                {
                  "key": "channel",
                  "value": "",
                  "description": "Origination channel.",
                  "disabled": true
                },
                {
                  "key": "settlementId",
                  "value": "",
                  "description": "All transactions in one settlement batch.",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "",
                  "description": "Free-text over references.",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "",
                  "description": "Cursor from the previous page.",
                  "disabled": true
                }
              ]
            },
            "description": "Your unified transaction feed. **Scope:** `transaction:read`.\n\nFilters (all optional, combinable):\n- `status` — `PaymentOperationStatus`: `PENDING`, `PENDING_3DS`, `SUCCESS`, `FAILED`, `CANCELED`. ⚠️ A completed payment is **`SUCCESS`** here, even though the webhook for the same event is named `payment.succeeded` — `SUCCEEDED` is not a valid value and returns `400`.\n- `type` — `PaymentOperationType`: `PAYMENT`, `REFUND`, `BILL_PAYMENT`, `BANK_TRANSFER`.\n- `method` — payment method, e.g. `CARD`, `CASHIN`.\n- `channel` — origination channel.\n- `settlementId` — everything included in one settlement batch. The fastest way to reconcile a payout against the sales behind it.\n- `search` — free-text over references.\n- `from` / `to` — ISO-8601 UTC instants.\n\nPaging: **cursor-based** via `cursor` + `limit` (use the cursor returned by the\nprevious page), with `page`/`size` also accepted."
          },
          "response": []
        },
        {
          "name": "Export transactions (CSV)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/transactions/export.csv?from=2026-08-01T00:00:00Z&to=2026-08-31T23:59:59Z",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "transactions",
                "export.csv"
              ],
              "query": [
                {
                  "key": "from",
                  "value": "2026-08-01T00:00:00Z"
                },
                {
                  "key": "to",
                  "value": "2026-08-31T23:59:59Z"
                },
                {
                  "key": "status",
                  "value": "SUCCESS",
                  "description": "PENDING, PENDING_3DS, SUCCESS, FAILED, CANCELED.",
                  "disabled": true
                },
                {
                  "key": "type",
                  "value": "PAYMENT",
                  "disabled": true
                },
                {
                  "key": "method",
                  "value": "CARD",
                  "disabled": true
                }
              ]
            },
            "description": "Downloads the filtered feed as CSV. **Scope:** `transaction:export` — a *separate* scope from `transaction:read`.\n\nSupports `status`, `type`, `method`, `from`, `to` (no cursor — it streams the\nwhole match). Use **Send and Download** in Postman."
          },
          "response": []
        },
        {
          "name": "Retrieve transaction",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/transactions/{{operationId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "transactions",
                "{{operationId}}"
              ]
            },
            "description": "One transaction by its numeric `operationId`. **Scope:** `transaction:read`.\n\nThis is the same `OperationId` webhooks carry and refunds accept."
          },
          "response": []
        },
        {
          "name": "Transaction timeline",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/transactions/{{operationId}}/timeline",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "transactions",
                "{{operationId}}",
                "timeline"
              ]
            },
            "description": "Every state change for one transaction, in order. **Scope:** `transaction:read`.\n\nThe first thing to open in a support ticket: it shows authorisation, 3DS, capture, settlement and refund steps with timestamps, so you can see exactly where a payment stalled."
          },
          "response": []
        }
      ],
      "description": "# Transactions — `/v1/transactions`\n\nThe reconciliation surface: one feed covering payments, refunds and everything\nelse that moved money.\n\n**Scopes:** `transaction:read`, plus `transaction:export` for CSV.\n\n**List** captures `{{operationId}}` from the first row, which folder `8` reuses.\n\nFor finance workflows, filter by `settlementId` to line a payout up against the\nsales that funded it."
    },
    {
      "name": "10 · Journey Analytics",
      "item": [
        {
          "name": "Journey events",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/analytics/journeys/{{journeyResourceType}}/{{journeyResourceId}}/events?page=0&size=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "analytics",
                "journeys",
                "{{journeyResourceType}}",
                "{{journeyResourceId}}",
                "events"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "0"
                },
                {
                  "key": "size",
                  "value": "20"
                }
              ]
            },
            "description": "Every tracked customer-journey event for one resource. **Scope:** `analytics:read`.\n\nPath parameters:\n- `resourceType` — a `JourneyResourceType`, e.g. `PAYMENT_LINK`, `PAYMENT_SESSION`.\n- `resourceId` — the resource's reference, e.g. a `pl_…` or `ps_…`.\n\nPaginated. Answers \"the buyer opened the link — then what?\": page views, method\nselection, failed attempts, abandonment."
          },
          "response": []
        },
        {
          "name": "Journey summary",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/analytics/journeys/{{journeyResourceType}}/{{journeyResourceId}}/summary",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "analytics",
                "journeys",
                "{{journeyResourceType}}",
                "{{journeyResourceId}}",
                "summary"
              ]
            },
            "description": "Aggregated funnel for one resource. **Scope:** `analytics:read`.\n\nThe condensed version of the events feed — counts and drop-off rather than the raw stream."
          },
          "response": []
        }
      ],
      "description": "# Journey analytics — `/v1/analytics/journeys`\n\nConversion telemetry for a single payment link or checkout session: what the\nbuyer did between opening the page and paying (or not).\n\n**Scope:** `analytics:read`.\n\nSet `{{journeyResourceType}}` (e.g. `PAYMENT_LINK`) and `{{journeyResourceId}}`\n(e.g. the `pl_…` reference captured in folder `1`) before running these."
    },
    {
      "name": "11 · Webhooks",
      "item": [
        {
          "name": "Register a webhook endpoint",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (b.id) pm.collectionVariables.set('webhookEndpointId', b.id);",
                  "if (b.signingSecret) { pm.collectionVariables.set('webhookSecret', b.signingSecret); console.log('Signing secret stored — it is shown only once.'); }"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://merchant.example.com/webhooks/charipay\",\n  \"description\": \"Postman sandbox receiver\",\n  \"enabledEvents\": [\n    \"payment.succeeded\",\n    \"payment.failed\",\n    \"refund.succeeded\",\n    \"refund.failed\",\n    \"subscription.payment_succeeded\",\n    \"subscription.payment_failed\",\n    \"subscription.canceled\",\n    \"order.paid\"\n  ],\n  \"apiVersion\": \"2024-01-01\",\n  \"enabled\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/partner/webhooks/endpoints",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "partner",
                "webhooks",
                "endpoints"
              ]
            },
            "description": "Registers a signed receiver for asynchronous events. **Scope:** `webhook:manage`.\n\nBody (`WebhookEndpointRequest`):\n- `url` *(required)* — public **HTTPS on port 443**. Private, loopback, link-local and plain-HTTP targets are rejected outright, so `localhost` will not work: use a tunnel (ngrok, Cloudflare Tunnel) or a request-bin during development.\n- `description` — ≤ 500 chars.\n- `enabledEvents` — **explicit allowlist, strongly recommended.** Null or empty subscribes you to every current *and future* event type, so a new event we ship later starts arriving unannounced.\n  > ⚠️ In **this JSON body** an event is its **wire value** — `\"payment.succeeded\"`, lowercase and dotted. The query parameters on *List delivery attempts* take the **enum name** instead — `PAYMENT_SUCCEEDED`. The two are not interchangeable; sending a name here fails validation.\n- `customHeaders` — static routing headers. `Host`, `Authorization`, `Cookie`, `Chari-*` and `X-CHARI-*` cannot be overridden.\n- `apiVersion` — payload contract version; defaults to `2024-01-01`.\n- `enabled` — start delivering immediately. Defaults to `true`.\n- `environment` — **ignored on this partner route.** Your API key decides, so a sandbox key can never register a production endpoint.\n\n> 🔑 The `whsec_…` signing secret is returned **once**, at creation. Store it —\n> you cannot read it back, only rotate it."
          },
          "response": []
        },
        {
          "name": "List webhook endpoints",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/partner/webhooks/endpoints?page=0&size=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "partner",
                "webhooks",
                "endpoints"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "0"
                },
                {
                  "key": "size",
                  "value": "20"
                }
              ]
            },
            "description": "Your registered endpoints, paginated. **Scope:** `webhook:read`.\n\nSecrets are never returned here — only at creation and rotation."
          },
          "response": []
        },
        {
          "name": "Retrieve webhook endpoint",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/partner/webhooks/endpoints/{{webhookEndpointId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "partner",
                "webhooks",
                "endpoints",
                "{{webhookEndpointId}}"
              ]
            },
            "description": "One endpoint by UUID, with its health counters. **Scope:** `webhook:read`."
          },
          "response": []
        },
        {
          "name": "Update webhook endpoint",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://merchant.example.com/webhooks/charipay\",\n  \"description\": \"Postman sandbox receiver (updated)\",\n  \"enabledEvents\": [\n    \"payment.succeeded\",\n    \"payment.failed\",\n    \"refund.succeeded\"\n  ],\n  \"enabled\": true\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/partner/webhooks/endpoints/{{webhookEndpointId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "partner",
                "webhooks",
                "endpoints",
                "{{webhookEndpointId}}"
              ]
            },
            "description": "Updates URL, allowlist, headers or enabled flag. **Scope:** `webhook:manage`.\n\nThe body is the same `WebhookEndpointRequest`. `environment` is ignored — an endpoint **cannot move between sandbox and production**; delete it and re-create instead."
          },
          "response": []
        },
        {
          "name": "Send a test event",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/partner/webhooks/endpoints/{{webhookEndpointId}}/test",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "partner",
                "webhooks",
                "endpoints",
                "{{webhookEndpointId}}",
                "test"
              ]
            },
            "description": "Delivers a synthetic event to the endpoint right now. **Scope:** `webhook:manage`.\n\nThe fastest way to prove your receiver is reachable and your signature check is correct, before any real money moves."
          },
          "response": []
        },
        {
          "name": "Activate endpoint",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/partner/webhooks/endpoints/{{webhookEndpointId}}/activate",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "partner",
                "webhooks",
                "endpoints",
                "{{webhookEndpointId}}",
                "activate"
              ]
            },
            "description": "Re-enables an endpoint. **Scope:** `webhook:manage`.\n\nRepeated delivery failures disable an endpoint automatically; fix your receiver, then call this."
          },
          "response": []
        },
        {
          "name": "Rotate signing secret",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "if (b.signingSecret) pm.collectionVariables.set('webhookSecret', b.signingSecret);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/partner/webhooks/endpoints/{{webhookEndpointId}}/rotate-secret",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "partner",
                "webhooks",
                "endpoints",
                "{{webhookEndpointId}}",
                "rotate-secret"
              ]
            },
            "description": "Issues a new `whsec_…` secret and returns it **once**. **Scope:** `webhook:manage`.\n\nDeploy the new secret to your receiver promptly — deliveries signed with the old one will start failing verification."
          },
          "response": []
        },
        {
          "name": "Delete webhook endpoint",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/partner/webhooks/endpoints/{{webhookEndpointId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "partner",
                "webhooks",
                "endpoints",
                "{{webhookEndpointId}}"
              ]
            },
            "description": "Removes the endpoint permanently. **Scope:** `webhook:manage`. Returns `204 No Content`."
          },
          "response": []
        },
        {
          "name": "List delivery attempts",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const b = pm.response.json();",
                  "const rows = b.content || [];",
                  "if (rows.length && rows[0].id) pm.collectionVariables.set('webhookDeliveryId', rows[0].id);"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/partner/webhooks/events?page=0&size=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "partner",
                "webhooks",
                "events"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "0"
                },
                {
                  "key": "size",
                  "value": "20"
                },
                {
                  "key": "endpointId",
                  "value": "{{webhookEndpointId}}",
                  "description": "Filter to one endpoint.",
                  "disabled": true
                },
                {
                  "key": "eventType",
                  "value": "PAYMENT_SUCCEEDED",
                  "description": "Enum NAME here (PAYMENT_SUCCEEDED), not the dotted wire value used in request bodies.",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "FAILED",
                  "description": "PENDING, SENDING, DELIVERED, FAILED, RETRYING, EXHAUSTED, SKIPPED (enum name).",
                  "disabled": true
                },
                {
                  "key": "from",
                  "value": "2026-08-01T00:00:00Z",
                  "disabled": true
                },
                {
                  "key": "to",
                  "value": "2026-08-31T23:59:59Z",
                  "disabled": true
                }
              ]
            },
            "description": "Delivery log — what we sent, when, and how your server answered. **Scope:** `webhook:read`.\n\nFilters: `endpointId`, `eventType`, `status`, `from`, `to` (ISO-8601 UTC), plus\npaging. This is where you look when you believe an event never arrived.\n\n> Query-parameter enums use the **enum name** (`PAYMENT_SUCCEEDED`), unlike the\n> `enabledEvents` **body** field, which uses the dotted wire value\n> (`payment.succeeded`)."
          },
          "response": []
        },
        {
          "name": "Retrieve a delivery attempt",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/partner/webhooks/events/{{webhookDeliveryId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "partner",
                "webhooks",
                "events",
                "{{webhookDeliveryId}}"
              ]
            },
            "description": "One delivery by id, with the exact payload, response code, body and latency. **Scope:** `webhook:read`."
          },
          "response": []
        },
        {
          "name": "List event types",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-CHARI-PAY-API-KEY",
                "value": "{{apiKey}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/partner/webhooks/event-types",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api",
                "v1",
                "partner",
                "webhooks",
                "event-types"
              ]
            },
            "description": "The catalogue of event types an endpoint can subscribe to — read it before hard-coding your subscription list."
          }
        }
      ],
      "description": "# Webhooks — `/api/v1/partner/webhooks`\n\nPayments are asynchronous. **Polling is a fallback; the webhook is the source of\ntruth.** Register a receiver here, verify every signature, and treat the event as\nthe moment money moved.\n\n**Scopes:** `webhook:manage`, `webhook:read`. The API key's own environment\ndecides whether the endpoint is a sandbox or production one.\n\n## How a delivery looks\n\nEach POST to your URL carries:\n\n| Header | Meaning |\n|---|---|\n| `X-CHARI-SIGNATURE` | lowercase hex HMAC-SHA256 over `timestamp + \".\" + rawBody` |\n| `X-CHARI-SIGNATURE-NEXT` | **only during a secret rotation** — the same body signed with the *new* secret |\n| `X-CHARI-TIMESTAMP` | epoch **milliseconds** used in that HMAC |\n| `Chari-Event-Id` | **the idempotency key — deduplicate on this.** Same value as `WebhookEventId` in the body |\n| `Chari-Webhook-Id` | id of this *delivery attempt*; changes on retry, so it is **not** the dedup key |\n| `Chari-Event-Type` | e.g. `payment.succeeded` |\n| `Chari-Retry-Count` | attempt number |\n\n**`Chari-Event-Id` vs `Chari-Webhook-Id`.** One logical event can be delivered\nseveral times; each attempt gets its own `Chari-Webhook-Id` but they all carry\nthe same `Chari-Event-Id`. Deduplicating on the wrong one means you process the\nsame payment twice.\n\n## Verifying (Node)\n\n```js\nconst crypto = require('crypto');\n\nfunction verify(rawBody, signature, timestamp, secret) {\n  // ±5 minutes anti-replay window\n  if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) return false;\n  const expected = crypto\n    .createHmac('sha256', secret)\n    .update(`${timestamp}.${rawBody}`)\n    .digest('hex');\n  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));\n}\n```\n\nFour rules that matter:\n1. **HMAC the raw bytes**, before any JSON parsing — re-serialising changes them.\n2. **Reject timestamps skewed more than ±5 minutes**; we do the same.\n3. **Compare in constant time** (`timingSafeEqual`), never `===`.\n4. **During a rotation, accept either signature.** While the old secret is still\n   in its grace window we send `X-CHARI-SIGNATURE` (old) *and*\n   `X-CHARI-SIGNATURE-NEXT` (new). Verify against whichever secret you hold, then\n   cut over.\n\nDeduplicate atomically on **`Chari-Event-Id`** — delivery is at-least-once and\ncan be out of order, so you *will* see repeats. Return `2xx` only after both the\nbusiness update and the dedup record have committed; any non-2xx is retried.\n\n## Event catalog\n\nThese are the events **actually emitted by this release** — the complete list.\nOther values visible in the `WebhookEventType` enum are reserved and never fire,\nso do not build against them.\n\n| Event | Fires when |\n|---|---|\n| `payment.succeeded` | payment captured |\n| `payment.failed` | payment declined or errored |\n| `subscription.payment_succeeded` | a subscription period was collected |\n| `subscription.payment_failed` | automatic collection failed |\n| `subscription.canceled` | subscription ended |\n| `refund.succeeded` / `refund.failed` | refund resolved |\n| `order.paid` | storefront order paid |\n| `merchant_transfer.completed` / `.failed` | wallet-to-bank transfer |\n| `topup.succeeded` / `topup.failed` | telco top-up |\n| `bill_payment.pending` / `.succeeded` / `.failed` | bill payment |\n| `wallet.activated` / `wallet.rejected` | sub-merchant wallet KYC decision |\n| `payout.completed` / `payout.failed` | sub-merchant payout |\n\nYour `metadata` and `externalId` come back on the events for the resource that\ncarried them — that is how you reconcile without storing our references.\nSubscription events additionally echo `ExternalId`, `ClientId`, `PeriodDate` and\n`ChargeReference`, so you can pin down the exact customer and billing period."
    }
  ],
  "auth": {
    "type": "apikey",
    "apikey": [
      {
        "key": "key",
        "value": "X-CHARI-PAY-API-KEY",
        "type": "string"
      },
      {
        "key": "value",
        "value": "{{apiKey}}",
        "type": "string"
      },
      {
        "key": "in",
        "value": "header",
        "type": "string"
      }
    ]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Auto-correlation: every request carries an X-Request-Id, echoed on the response.",
          "if (!pm.request.headers.has('X-Request-Id')) {",
          "    pm.request.headers.add({ key: 'X-Request-Id', value: 'req_' + pm.variables.replaceIn('{{$guid}}') });",
          "}",
          "",
          "// Retry safety: give every POST an Idempotency-Key unless one is already set.",
          "if (pm.request.method === 'POST' && !pm.request.headers.has('Idempotency-Key')) {",
          "    pm.request.headers.add({ key: 'Idempotency-Key', value: 'idem_' + pm.variables.replaceIn('{{$guid}}') });",
          "}"
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Surface the error envelope in the Postman console instead of silent failures.",
          "if (pm.response.code >= 400) {",
          "    let body;",
          "    try { body = pm.response.json(); } catch (e) { body = null; }",
          "    if (body && body.error) {",
          "        console.error('[' + pm.response.code + '] ' + body.error.code + ': ' + body.error.message +",
          "                      (body.correlationId ? ' (correlationId ' + body.correlationId + ')' : ''));",
          "    }",
          "}"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api-psp.charipay.ma",
      "description": "Single base URL shared by sandbox and production — the API key, not the URL, decides the environment."
    },
    {
      "key": "apiKey",
      "value": "",
      "description": "Your API key. Captured automatically by folder 0, step 6."
    },
    {
      "key": "jwt",
      "value": "",
      "description": "Portal JWT. Captured by folder 0, step 3."
    },
    {
      "key": "refreshToken",
      "value": "",
      "description": "Portal refresh token. Captured by folder 0, step 3."
    },
    {
      "key": "stepUpToken",
      "value": "",
      "description": "Single-use step-up token. Captured by folder 0, step 5."
    },
    {
      "key": "signupEmail",
      "value": "you+charipay@example.com",
      "description": "The e-mail you sign up with; also your portal login."
    },
    {
      "key": "portalPassword",
      "value": "ChangeMe!2026",
      "description": "Password you set during activation (8-128 chars)."
    },
    {
      "key": "activationToken",
      "value": "",
      "description": "PASTE MANUALLY: the token from the activation e-mail link."
    },
    {
      "key": "deviceId",
      "value": "postman-workstation-1",
      "description": "Stable device fingerprint — avoids the new-device e-mail gate on every login."
    },
    {
      "key": "onboardingCaseId",
      "value": "",
      "description": "Captured by folder 0, step 1."
    },
    {
      "key": "apiKeyId",
      "value": "",
      "description": "Captured by folder 0, step 6."
    },
    {
      "key": "linkReference",
      "value": "",
      "description": "pl_… — captured by Payment Links / Create."
    },
    {
      "key": "payUrl",
      "value": "",
      "description": "Hosted payment page for the last created link."
    },
    {
      "key": "sessionId",
      "value": "",
      "description": "ps_… — captured by Checkout Sessions / Create."
    },
    {
      "key": "verifyKey",
      "value": "",
      "description": "Single-use vk for the last session."
    },
    {
      "key": "checkoutUrl",
      "value": "",
      "description": "Hosted checkout page for the last session."
    },
    {
      "key": "operationId",
      "value": "",
      "description": "Numeric transaction id — captured by Checkout/Submit or Transactions/List."
    },
    {
      "key": "paymentReference",
      "value": "",
      "description": "Public payment reference, e.g. ord_7Hq2Lm."
    },
    {
      "key": "clientId",
      "value": "",
      "description": "Client UUID — captured by Clients / Create."
    },
    {
      "key": "paymentMethodId",
      "value": "",
      "description": "Stored-card UUID — captured by Clients / List saved payment methods."
    },
    {
      "key": "subscriptionRef",
      "value": "",
      "description": "sub_… — captured by Subscriptions / Create."
    },
    {
      "key": "productReference",
      "value": "",
      "description": "prod_… — captured by Products / Create."
    },
    {
      "key": "refundReference",
      "value": "",
      "description": "Refund reference — captured by Refunds / Create."
    },
    {
      "key": "webhookEndpointId",
      "value": "",
      "description": "Endpoint UUID — captured by Webhooks / Register."
    },
    {
      "key": "webhookSecret",
      "value": "",
      "description": "whsec_… signing secret — returned only at creation or rotation."
    },
    {
      "key": "webhookDeliveryId",
      "value": "",
      "description": "Delivery id — captured by Webhooks / List delivery attempts."
    },
    {
      "key": "journeyResourceType",
      "value": "PAYMENT_LINK",
      "description": "PAYMENT_LINK, PAYMENT_SESSION, … for the analytics folder."
    },
    {
      "key": "journeyResourceId",
      "value": "{{linkReference}}",
      "description": "Reference of the resource to analyse."
    },
    {
      "key": "companySelectionToken",
      "value": "",
      "description": "Short-lived token from the login response. Captured by folder 0, step 3."
    },
    {
      "key": "enterpriseId",
      "value": "",
      "description": "Your enterprise id from the login response. Captured by folder 0, step 3."
    }
  ]
}
