Skip to main content
All articles
Technical11 min read

Sub-merchant API in Morocco: crediting a sub-merchant's wallet and tracking its webhooks

Sub-merchant API in Morocco: create a session that credits a wallet with config.settlement, track wallet.activated and payout events, and handle errors.

With the sub-merchant API in Morocco, collecting on behalf of a sub-merchant takes an ordinary checkout session: create it as you would for any payment and add a settlement object inside config, naming the wallet to credit — by its walletId or by the sub-merchant's subMerchantReference. The buyer pays on the hosted checkout, and your system follows the rest through signed webhooks: wallet.activated and wallet.rejected for verification, payout.completed and payout.failed for payouts, submerchant.near_cap for balance caps. This guide covers the request, the events, declaring the endpoint, the errors to expect, what happens in the portal versus the API, and reconciliation per sub-merchant.

Sub-merchant API: wallet, walletId and subMerchantReference

ChariPay is a Moroccan payment platform, a PSP operated by Chari Money, a payment institution licensed by Bank Al-Maghrib. Its distribution space is for aggregators, franchises and agent networks. A sub-merchant is an entity you create in your distribution space, a seller or a store for example; it is verified like any merchant — documents and trade register — before collecting in production, and has its own wallet.

On the API side, four notions are enough:

TermWhat it isFormat
Sub-merchantEntity created in your distribution space, verified like any merchantDeclared from the portal or through the API
WalletThe sub-merchant's own wallet, credited by the sessions that name itOne per sub-merchant
walletIdID of the sub-merchant wallet to creditUUID, for example 3fa85f64-5717-4562-b3fc-2c963f66afa6
subMerchantReferenceReference of the sub-merchant to creditString, for example subm_9Zt2Bc

Both identifiers go into config.settlement, an optional object on the session: send one or the other, whichever your database already stores. That object's only purpose is to name a sub-merchant wallet to credit.

A sub-merchant is declared from the portal or through the API; the public API contract does not document that call, which is set up with the integration team. The offer is described on the distribution and sub-merchants page, and the business model of a multi-seller platform in marketplace payments.

Creating a session that credits a sub-merchant

The request is that of a regular checkout session, with an extra settlement block inside config:

bash
curl -X POST 'https://api-psp.charipay.ma/v1/payment-sessions' \
  -H 'X-CHARI-PAY-API-KEY: chari_sk_test_...' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: order-2026-0421-session' \
  -d '{
    "amount": 250.00,
    "orderId": "ORD-2026-0421",
    "externalId": "order-2026-0421",
    "config": {
      "customer": {
        "email": "amine.bennani@example.com",
        "firstName": "Amine",
        "lastName": "Bennani",
        "phone": "+212600000000"
      },
      "urls": {
        "accept": "https://votre-boutique.ma/paiement/succes",
        "decline": "https://votre-boutique.ma/paiement/echec",
        "notification": "https://votre-boutique.ma/webhooks/charipay"
      },
      "settlement": {
        "subMerchantReference": "subm_9Zt2Bc"
      }
    },
    "metadata": { "cartId": "c_987", "source": "web" }
  }'

The response (201, or 200 on a replay of the same externalId) contains these five fields and nothing else:

json
{
  "sessionId": "ps_5Kd0Rn",
  "sessionToken": "st_…",
  "verifyKey": "vk_5Kd0Rn",
  "expiresAt": "2026-10-05T10:15:00Z",
  "checkoutUrl": "https://…/checkout/ps_5Kd0Rn"
}

Redirect the buyer to checkoutUrl: they pay by card with 3-D Secure on the hosted page. The response returns no status, no amount and no sub-merchant: status and amount are read with GET /v1/payment-sessions/{sessionId}, and it is up to your database to keep the link between sessionId and sub-merchant. Each session has a limited lifetime — 72 hours by default, expiresAt to shorten it — and is consumed only once. Confirm the order when the webhook arrives, never on the redirect alone. Fields and responses are detailed in the Checkout sessions module of the API docs.

You can write this first call today: Start in test mode. Three fields — name, work email, company — an activation link received by email to choose your password, then the portal, where you create your own test key after re-authenticating; the full key is shown only once. Pay with the test card 4918 9141 0719 5005, CVV 123, any future expiry date, 3-D Secure code 555. You can create checkout sessions right away; the distribution setup — and therefore the sub-merchants that config.settlement names — is prepared with the integration team.

Sub-merchant lifecycle events

Five webhook events concern distribution. Their name says what they signal; how you react is up to you:

EventWhat it signalsSuggested reaction
wallet.activatedA sub-merchant's wallet is activated after verificationMark the sub-merchant "active" in your database and open its sales
wallet.rejectedA sub-merchant's wallet is rejected at verificationBlock its sales, notify it, revisit its application
payout.completedA payout has been executedMark the payout as executed in your accounting
payout.failedA payout has failedAlert the finance team and investigate before any new request
submerchant.near_capA sub-merchant is approaching its balance capWarn the store manager or the finance team

Payments themselves arrive as payment.succeeded. Always route on the Chari-Event-Type header, and read the event ID from Chari-Event-Id. Deduplicate on Chari-Event-Id, not on Chari-Webhook-Id — the latter identifies the delivery, not the event. Verify the X-CHARI-SIGNATURE signature with X-CHARI-TIMESTAMP before any processing, and build nothing on body fields beyond your own metadata. The catalog and delivery rules are in the Webhooks module of the API docs.

Declaring the endpoint that receives these events

A webhook endpoint is declared with POST /api/v1/partner/webhooks/endpoints, using the key of the target environment. Subscribe it to an explicit list of events: an empty list means a subscription to every event, current and future.

bash
curl -X POST 'https://api-psp.charipay.ma/api/v1/partner/webhooks/endpoints' \
  -H 'X-CHARI-PAY-API-KEY: chari_sk_test_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://votre-boutique.ma/webhooks/charipay",
    "description": "Paiements boutique",
    "enabledEvents": [
      "payment.succeeded",
      "wallet.activated",
      "wallet.rejected",
      "payout.completed",
      "payout.failed",
      "submerchant.near_cap"
    ]
  }'

Three rules to know:

  • the url must be public HTTPS on port 443: localhost, private addresses and plain HTTP are rejected at registration — in development, go through a tunnel;
  • the response carries the signingSecret (whsec_…) only once, at creation or rotation: store it in a vault immediately;
  • an endpoint belongs to one environment, and secrets differ between sandbox and production.

The same declaration can be made from the portal. For local debugging — tunnel, test event, delivery log — see testing your webhooks locally.

Errors to plan for

Every /v1 API error shares the same envelope:

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "amount: must be greater than 0"
  },
  "correlationId": "b0c1e2d3-4f56-7890-abcd-ef0123456789"
}

Test the code, never the message, and log the correlationId. The codes that matter for this flow:

StatusCodeWhat it meansWhat to do
400VALIDATION_ERRORThe request is malformed or failed validationFix the request before sending it again; do not replay it as is
401UNAUTHORIZEDMissing or invalid API keyCheck the X-CHARI-PAY-API-KEY header and the key's environment
403PRODUCTION_ACCESS_NOT_ENABLEDA chari_sk_live_… key used before production is enabledStay in test mode until activation
409IDEMPOTENCY_CONFLICTSame Idempotency-Key, different bodyGenerate a new key for a new request
422WALLET_NOT_ACTIVEValid request, but a business rule blocks itDo not redirect the buyer; log it and check with the integration team if it persists

The contract gives WALLET_NOT_ACTIVE as a generic example of a 422 code on most of its endpoints: do not read a more specific cause into it than its name. Remember too that a replay on an existing externalId returns 200 OK instead of 201 Created: both are successes, so test the status.

What happens in the portal, what happens through the API

OperationWhere it happensWhat your system sees
Declare a sub-merchant, one by one or in bulkPortal or APIThe sub-merchant and its own wallet
Verify the sub-merchantBuilt in: documents and trade registerwallet.activated or wallet.rejected
Collect for a sub-merchantAPI: session with config.settlementpayment.succeeded, with your metadata
Receive eventsEndpoint declared through the API or from the portalChari-Event-Type and Chari-Event-Id headers
Pay outPayouts under dual approval: distinct requester and approverpayout.completed or payout.failed
Cap the balanceBalance caps defined at setupsubmerchant.near_cap

Everything else — allocation, automatic collection frequency, how approvers are organized — is prepared with the integration team. Keep the boundary in mind: for distribution, the public API contract covers the session with config.settlement and the events; the rest follows the governance set at setup.

Reconciling collections per sub-merchant

The creation response does not name the sub-merchant, and no undocumented field in the event body should be used to find it. So make each payment self-describing: add the sub-merchant reference to your metadata — for example "subMerchantReference": "subm_9Zt2Bc" next to cartId. Your metadata is echoed verbatim in the payment event, and a session's event also carries your orderId. Find your order through these two values: a session's externalId does not come back in the event.

Two design rules are enough:

  1. 1When you create the session, store the sessionId → sub-merchant → order row in your database.
  2. 2When payment.succeeded arrives, find that row through your metadata and mark the order paid.

After your server has been unavailable, reconcile with GET /v1/transactions: polling is a fallback, not the source of truth. A monthly statement is also produced for each sub-merchant, covering only its own activity.

Going live with a sub-merchant network

Once KYB is validated — identity documents and trade register, with dual human review — your application is reviewed and you receive a priced proposal, then your account is activated and your production key created. In your code, you change only the key and the webhook secret: URLs, payloads and error codes are identical. Until production is enabled, a chari_sk_live_… key receives 403 PRODUCTION_ACCESS_NOT_ENABLED. Keep the sandbox as your staging environment, with two sets of environment variables.

For distribution, the setup — sub-merchants, caps, collection, approvers — is prepared with the integration team; we make no promise here about how it behaves in the sandbox. For the rest of the API — payment links, refunds, transactions — see payment API in Morocco.

Frequently asked questions

What is the difference between walletId and subMerchantReference?

Both name the destination of a payment in config.settlement. walletId is the ID of the sub-merchant's wallet, in UUID format; subMerchantReference is the sub-merchant's reference, for example subm_9Zt2Bc. Send one or the other, whichever your database already stores. The settlement block is optional: its only purpose is to name a sub-merchant wallet. Since both fields serve the same purpose, pick one and stick to it across all your calls: your reconciliation will be simpler for it.

Can a sub-merchant be declared through the API?

Yes: declaration happens from the portal or through the API, one by one or in bulk. The public API contract does not document that call; it is set up with the integration team when the distribution setup is put in place. Either way, the sub-merchant is verified like any merchant — documents and trade register — before collecting in production.

What should I do if session creation returns a 422 error?

Do not redirect the buyer. A 422 means the request is valid but a business rule blocks it; the contract cites WALLET_NOT_ACTIVE as an example, and its name says no more than that. Read the code, log the correlationId, check the status of the named sub-merchant in your database, and check with the integration team if the code persists.

Which webhooks should I follow for a sub-merchant network?

payment.succeeded for payments, then the five distribution events: wallet.activated and wallet.rejected for verification, payout.completed and payout.failed for payouts, submerchant.near_cap for caps. Subscribe your endpoint to this explicit list rather than to every event, route on Chari-Event-Type and deduplicate on Chari-Event-Id. Verify the signature before any processing, and build nothing on body fields beyond your own metadata.

How do I know a sub-merchant has been approved?

Through the wallet.activated event, which signals that its wallet has been activated after verification; wallet.rejected signals a rejection instead. We publish no verification timeline. Wire these two events to the sub-merchant's status in your database, and only open its sales in production after activation. Until then, keep the sub-merchant in a "pending verification" status on your platform.

Does the session creation response show which sub-merchant is credited?

No. The response contains five fields and nothing else: sessionId, sessionToken, verifyKey, expiresAt and checkoutUrl. Status and amount are then read with GET /v1/payment-sessions/{sessionId}. Keep the link between sessionId and sub-merchant yourself, and add its reference to your metadata, which is echoed verbatim in the payment event: you will then find the sub-merchant in every payment event, with no manual lookup.

How do I find a sub-merchant's payments?

Through your own database, fed by the events: put the sub-merchant reference in your metadata, which is echoed verbatim in the payment event, and a session's event also carries your orderId. After downtime, reconcile with GET /v1/transactions. A monthly statement is also produced for each sub-merchant, limited to its own activity.

Next step

Write your sessions and your webhook receiver against the sandbox: Start in test mode, then pay with the test card. In parallel, prepare the distribution setup with the integration team, starting from the distribution and sub-merchants page and the Checkout sessions module of the API docs.

Written by ChariPay team.

Read next

Ready to try it yourself?

Create your account and take test-mode payments today: it is free, with no approval to wait for. A question? Our team supports merchants and developers alike.

  • Free, no commitment
  • Test mode from sign-up
  • No approval to wait for