Skip to main content
All articles
Technical13 min read

Test payments in Morocco: the test card, test scenarios and what the sandbox covers

Test payments in Morocco with no real money: the test card, the 3-D Secure code, a scenario-based test plan and what the sandbox does not cover.

To test payments in Morocco without real money, create a chari_sk_test_… key, call the same API you will use in production, and pay with the test card 4918 9141 0719 5005, CVV 123, any future expiry date and the 3-D Secure code 555. The flow is complete — hosted payment page, authentication, signed webhook, refund, export — but not a single dirham moves. This guide gives you the card and the code, what the sandbox covers module by module, a six-scenario test plan to run before any launch, and what cannot be tested, with another way to check it.

Test mode: the same API, a test key

ChariPay is a Moroccan payment platform, a PSP operated by Chari Money, a payment institution licensed by Bank Al-Maghrib. Its test mode is not a mock-up: it is the same API, on the same base https://api-psp.charipay.ma, with the same paths, the same payloads and the same error codes as production. What changes is the key. A key starting with chari_sk_test_ works in the sandbox, a chari_sk_live_ key in production: you do not pick the environment in the URL or in a header, you pick it by picking the key. The two environments are isolated: the same externalId can exist once in each.

Test mode opens as soon as your account is activated, with no approval to wait for: Start in test mode. Three fields — name, work email, company — then 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 and is never sent by email: store it right away in an environment variable or a secrets vault, never in your repository. Your business verification (KYB) runs in parallel and only gates the move to production.

Test mode is free, with no time limit and no commitment; the pricing page details what only applies once you go live. For the conventions shared by every call — authentication, pagination, idempotency, errors — the developer hub and the API documentation are the reference.

The test card and the 3-D Secure code

Here is the card to enter on the payment page, as a customer would; it triggers the real flow — payment page, 3-D Secure authentication, webhook.

FieldValue to enter
Card number4918 9141 0719 5005
CVV123
Expiry dateany future date
3-D Secure code555

Only this card is accepted, and no real money moves. Any other card — yours, a colleague's, a test number copied from another platform — is rejected with a 502 and the BAAS_CHARI_ERROR code. If you see that response while testing, check the card you entered before hunting for a bug in your code; every other error response is explained one by one in payment API error codes.

No "decline" card is published: the sandbox accepts only one, this one. Failures are therefore triggered through the flow itself — abandonment, expiry, a malformed request — as the test plan below shows. The step you go through with code 555 is the one your customers will go through with their bank. Card payments go through 3-D Secure authentication: the cardholder confirms with their bank, which protects the merchant against most unpaid charges and disputes. What that step changes for your real sales is covered in 3-D Secure and success rates.

What the payment sandbox covers, module by module

Six areas can be tested end to end with a test key. For each, here is how to trigger it and what to look at.

ModuleHow to trigger itWhat you check
Payment linksPOST /v1/payment-links, or from the portalthe payUrl opens, the test card goes through, the link turns "Paid"
Checkout sessionsPOST /v1/payment-sessions, then redirect to checkoutUrlreturn to accept, signed event, status read with GET /v1/payment-sessions/{sessionId}
SubscriptionsPOST /v1/clients, then POST /v1/subscriptions with DAILY and today's startDatefirst payment link in currentCharge, paid with the test card, subscription.payment_succeeded, second installment the next day
RefundsPOST /v1/refunds, or from the portal202 on creation, 200 on replay, refund.succeeded or refund.failed
Webhooksendpoint registered with the test keyevents sent as usual, signature verified, delivery log
Exportsportal, or GET /v1/transactions/export.csvreferences present, timestamps in UTC

This list is the sandbox's promised scope. Webhooks go out exactly as they do in production, signed with the sandbox endpoint's secret: your receiver can get things wrong there at no cost.

How to test online payments end to end, step by step

Start with the flow of an order placed on your website: a checkout session. Create it server-side, with your test key:

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"
      }
    },
    "metadata": { "cartId": "c_987", "source": "web" }
  }'

The response — 201, or 200 if you replay 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"
}

Status and amount are not in this response: you read them with GET /v1/payment-sessions/{sessionId}. Then walk through the flow the way your customer will:

  1. 1Store the `sessionId` next to your order, in an "awaiting payment" state.
  2. 2Open `checkoutUrl` exactly as the response returns it, in a browser, as the buyer would.
  3. 3Pay with the test card, then enter 555 at the 3-D Secure step.
  4. 4Land back on the `accept` URL: your page shows a waiting state, not a confirmation yet.
  5. 5Receive the signed `payment.succeeded` event on your sandbox endpoint, verify its signature, and confirm the order at that moment.
  6. 6Read the status again with GET /v1/payment-sessions/{sessionId} to check that your database and ours agree.

The order of steps 4 and 5 is not guaranteed, and that is exactly what the test should prove: confirm the order when the webhook arrives, never on the redirect alone — the redirect says the buyer came back, not that the payment is secured. Keep in mind, too, that the notification URL, like any registered endpoint, must be reachable over public HTTPS: on a development machine, you need a tunnel.

The variant without a session is the payment link.

bash
curl -X POST 'https://api-psp.charipay.ma/v1/payment-links' \
  -H 'X-CHARI-PAY-API-KEY: chari_sk_test_...' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: order-1042-link' \
  -d '{
    "amount": 149.90,
    "description": "Commande #1042",
    "singleUse": true,
    "externalId": "order-1042",
    "customerEmail": "amine.bennani@example.com",
    "customerPhone": "+212600000000",
    "metadata": { "cartId": "c_987" }
  }'

The response gives you the link's reference (pl_…) and its payUrl: open the payUrl, pay with the test card, then read the link's state with GET /v1/payment-links/{reference}. The payment API in Morocco guide puts these two calls back into the full integration.

A six-scenario test plan

One successful payment proves very little: edge cases are what break an integration on launch day. Replay them in the sandbox, in this order.

ScenarioHow to trigger itExpected result
1. Successful paymentsession, test card, code 555return to accept, signed payment.succeeded, order confirmed when the event arrives
2. Abandonment during authenticationenter the card, then close the tab at the 3-D Secure stepno confirmation, order still "awaiting payment", status read with GET /v1/payment-sessions/{sessionId}
3. Expired sessioncreate a session with an expiresAt a few minutes ahead, wait until it passesthe session can no longer be paid; in direct checkout, POST /checkout/verify returns 410 SESSION_EXPIRED; your order moves to the state you planned
4. Double clickreplay the creation with the same Idempotency-Key, then with the same externalIdthe first result comes back; 200 instead of 201 on externalId; no second session
5. Refundsfull refund on a first payment; partial with refundAmount on a second; replay the same refundReference202 on creation, 200 on replay, refund.succeeded, no double debit
6. Test eventfrom the portal or the API, on the sandbox endpointsignature accepted, fast 2xx response, no business side effect

A few rules make this plan reliable. Test the partial refund on a different payment from the full refund, and have each refund identify its payment by the operationId read from GET /v1/transactions. In scenario 4, also replay the same Idempotency-Key with a modified body, including a new externalId: the expected response is 409 IDEMPOTENCY_CONFLICT, and your code should treat it as a bug on your side, not as a network incident. If the externalId stays the same, the existing session comes back, with 200. The full mechanism is explained in payment idempotency.

Do not build any business flow that depends on payment.failed: that event is only sent if you asked for it when creating the session (notifyOnFailure). A failure is detected by reading the status, and an order without confirmation stays pending until its session expires. Finally, scenario 6 is covered in detail in testing your webhooks locally: HTTPS tunnel, test event, delivery log.

Subscriptions, webhooks and exports: the checks people forget

Subscriptions. A ChariPay subscription is a series of payment links, one per due date, emailed to the customer: you test it by shortening the calendar. Create the client (POST /v1/clients) and then the subscription (POST /v1/subscriptions) with today's startDate and the DAILY frequency: the first link is created immediately and returned in currentCharge, and the subscription stays INCOMPLETE until it is paid. Pay that link with the test card, on the hosted page with 3-D Secure: the subscription switches to ACTIVE and you receive subscription.payment_succeeded. The next day, a second installment opens with a new link, to be paid the same way. The fields, events and full test plan are detailed in subscriptions through the API.

Webhooks. Register an endpoint with your test key: it belongs to that key's environment and gets its own signing secret (whsec_…), separate from the production one. Your code must read that secret from the environment's configuration, never hard-code it, or, after the switch to production, your verification will reject every notification.

Exports. Export the transactions from your test run — from the portal or with GET /v1/transactions/export.csv — and check two things. Every movement carries the reference of the sale, refund or payout that explains it, and a refund shows up as a separate movement linked to the original transaction. Timestamps are in UTC; Morocco is UTC+1: your reconciliation tool must convert, or a payment made just after midnight lands on the previous day.

What cannot be tested, and how to check it another way

The sandbox does not promise everything. Three things stay outside its scope:

  • A real cash deposit at an agency. Create the single-use link with paymentMethod: "CASH" and check that your code can read its state. In production, as soon as the network confirms the deposit, the link turns "Paid" and you are notified in the portal; on the server side, read the link's state with GET /v1/payment-links/{reference}. The deposit itself does not happen in the sandbox.
  • A real bank transfer or a real bill payment. The portal's transfer flow runs — in the sandbox, any six-digit code validates the transfer — but no beneficiary and no biller receives anything.
  • Any card other than the test card. It gets 502 BAAS_CHARI_ERROR. How a real card behaves — authentication with the issuing bank, whether it is open to online payments — can only be seen in production.

These three flows are checked on real operations, once your account is activated: in the portal and in the export. The sandbox, for its part, does not go away after launch: it remains your staging environment. Keep two sets of environment variables and test every change in the sandbox before shipping it. When you go live, 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. The go-live checklist is in moving from sandbox to production.

Frequently asked questions

Which test card should I use to test an online payment?

The card 4918 9141 0719 5005, with CVV 123, any future expiry date and the 3-D Secure code 555. It is the only card accepted in the sandbox: it triggers the full flow — payment page, authentication, signed webhook — without any real money moving. Any other card is rejected with a 502 and the BAAS_CHARI_ERROR code.

Which 3-D Secure code do I enter in test mode?

Enter 555 at the authentication step, after typing in the test card. This step reproduces the one your customers go through in production with their bank: it checks that your flow handles the redirect to authentication and then the return to your accept URL. No other code is documented for the sandbox.

Can I test a payment with my real card in the sandbox?

No. In the sandbox, only the test card is accepted: a real card, whatever its bank, is rejected with a 502 and the BAAS_CHARI_ERROR code. How a real card behaves — authentication with the issuing bank, whether it is open to online payments — can only be observed in production, once your account is activated.

Do I have to wait for my business to be verified before using test mode?

No. Test mode opens as soon as your account is activated, with no approval to wait for: you create your test key from the portal and pay with the test card. Your business verification (KYB) — identity documents and trade register — runs in parallel and only gates the move to production. Your technical team can therefore build and test the integration while your application is reviewed.

How do I simulate a failed payment in test mode?

No "decline" card is published: you trigger the failure through the flow. Close the tab at the 3-D Secure step to simulate abandonment, or create a session with a near expiresAt and let it expire. Then check that no order is confirmed — pending after abandonment, in your planned state after expiry — and read the status with GET /v1/payment-sessions/{sessionId}. Do not build logic on payment.failed, which is only sent if you asked for it.

Can I test a cash payment at an agency in the sandbox?

Not the deposit itself: no real cash deposit happens in the sandbox. You can create the single-use link with paymentMethod: "CASH" and check that your code reads its state with GET /v1/payment-links/{reference}. In production, as soon as the network confirms the deposit, the link turns "Paid" and you are notified in the portal.

How do I test several subscription installments?

Create the subscription with today's startDate and the DAILY frequency, using your sandbox key. The first payment link is created immediately and returned in currentCharge: pay it with the test card, 3-D Secure code 555, and you see subscription.payment_succeeded on your endpoint. The next day, a second installment opens, with a new link: in three days, you have seen three installments. The full test plan is in the guide to subscriptions through the API.

Are webhooks sent in test mode?

Yes, as usual: test payments, refunds and subscription installments trigger their signed events to the endpoint registered with your test key. That endpoint belongs to the sandbox and has its own secret, separate from production. To check your receiver without a payment, send the test event: a synthetic payment.succeeded carrying Test: true, which matches no real payment.

Next step

Create your account and your test key — Start in test mode — then run the six scenarios of this plan on your own integration, in order, with the test card. Your event receiver comes next, with testing your webhooks locally, and the go-live checklist comes last, with moving from sandbox to production.

Written by ChariPay team.

Read next

Guide4 min read

Sandbox to production: the go-live list

Moving from sandbox to production: the checklist before you switch keys — signed webhooks, idempotency, secrets, the classic oversights.

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