Skip to main content
All articles
Technical12 min read

In-app payments in Morocco: a safe architecture for iOS and Android apps

Payment gateway for a mobile app in Morocco: your server creates the session, the app opens the hosted checkout, a signed webhook confirms. No key in the app.

In-app payments in Morocco, on iOS or Android, rest on one rule: the app never talks to the payment API directly. It sends the order to your server; your server creates a checkout session with its API key and returns only the hosted checkout address to the app; the customer pays there by card with 3-D Secure; then a signed webhook confirms the payment to your server, which the app queries to show the result. No key in the app, no card number in your hands. This guide walks through that flow step by step, with the server-side code, the return, the confirmation and testing.

In-app payments in Morocco: what the term actually covers

The phrase "payment app" covers two very different things. On one side are the apps consumers install to pay; on the other, the infrastructure that an app (for delivery, bookings, courses or services) uses to collect money from its own users. ChariPay is the second kind: ChariPay is a Moroccan payment platform, a PSP operated by Chari Money, a payment institution licensed by Bank Al-Maghrib. Your users install nothing extra: they pay inside your app's flow, on a page hosted by ChariPay.

The documented integration requires no mobile SDK. It relies on three building blocks you already have if you run a backend: your server, the ChariPay REST API and the hosted checkout. So nothing ships inside the binary, and the same server handles your iOS app, your Android app and your website.

The technical entry point is the developer hub; the Postman collection and the OpenAPI specification can be downloaded from the API documentation, and the payment API in Morocco guide covers the server-side integration.

The seven-step flow, from the app to the webhook

Here is the full architecture of an in-app payment, with no shortcuts:

  1. 1The app sends the order to your server: the cart and the user ID, nothing more. No key, no card data.
  2. 2Your server creates the session with ChariPay, using the X-CHARI-PAY-API-KEY header: the key never leaves the server.
  3. 3The server returns only the `checkoutUrl` to the app, the address of that session's payment page.
  4. 4The app opens the hosted page at that address.
  5. 5The customer pays: card with 3-D Secure, on the hosted page, confirming with their bank.
  6. 6The customer is sent back to your accept or decline URL, depending on the outcome.
  7. 7The signed webhook reaches your server, which marks the order paid; the app queries your server and shows the result.
StepActorWhat travels
1App → your serverThe order and the user, with no key and no card
2Your server → ChariPay APIAmount, orderId, customer, return URLs, metadata
3Your server → appThe checkoutUrl, nothing else
4–5Customer ↔ hosted page and their bankThe card and 3-D Secure authentication
6Hosted page → return URLThe user coming back, not proof of payment
7ChariPay → your server; app → your serverThe signed webhook; then the order status

Every arrow has a reason. The card is collected by the hosted page, not by a form in your app, and your server never sees its number. The key only travels between your server and ChariPay. And the truth about the payment arrives through the one signed channel: the webhook.

You can build this flow end to end with no real money: Start in test mode. Three fields (name, work email, company), an activation link sent 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. You put it on your development server and pay with the test card. It is free, with no time limit and no approval to wait for: your business verification (KYB) only gates the move to production.

Why the API key must never ship inside the app

One key per company and per environment, with per-module permissions (scopes); you create it yourself from the portal, after re-authenticating; the full key is shown only once and is never sent by email; two keys can coexist during a rotation, without downtime. A key is a secret: never in the browser, a mobile app, a git repository or a screenshot.

For an app, the reason is concrete: a published app can be downloaded and decompiled by anyone. A key hidden in the code, a config file or a build variable ends up readable. With it, a third party could create sessions, read your transactions or trigger refunds in your company's name.

The key also selects the environment: a single base URL, https://api-psp.charipay.ma, serves both the sandbox and production, and the key makes the difference: chari_sk_test_… for the sandbox, chari_sk_live_… for production. A misplaced test key is therefore less serious than a production key, but the rule is the same for both. If a key may have leaked, create a new one, switch your server to it, then retire the old one. Details are in protecting your API keys.

Server side: create the session and return only the checkoutUrl

When the app sends an order, your server creates the session. The request, in curl:

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 on replay) 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"
}

Your server stores sessionId next to the order and passes only the checkoutUrl to the app. The session's status and amount can then be read with GET /v1/payment-sessions/{sessionId}. In an app, put whatever links the session to your user into metadata (the cart ID, the platform with "source": "ios" or "android" instead of "web"): it is a flat object of 4 KB at most, echoed verbatim in the webhook.

Three conventions to follow: amounts are in MAD, in major units (250.00); every declared URL is a public https:// URL, or the call returns 400; an optional field you do not use is omitted, never null. The full field reference is in the Checkout sessions module of the API docs.

Returning to the app: public https URLs, nothing more

After payment, the hosted page sends the user to the session's accept or decline URL. These URLs are public https:// URLs; if you do not provide them, the defaults configured on your account apply, then the platform's. In practice, point them to pages on your domain designed for the app: "verifying your payment" and "payment not completed".

The key point: these URLs bring the user back; they do not prove the payment. 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. An app that shows "paid" because it saw the accept URL go by will one day show "paid" for a payment that is not.

How the app opens the page and gets control back depends on your technical choices and target platforms, and we do not describe those settings here. Stick to the principle (hosted page opened at the address received, never in a hidden iframe, return through an https URL) and test the return on your target devices. A session lives 72 hours by default (expiresAt to shorten it) and is consumed only once; what happens if the user closes the page is covered in checkout session expiry and return.

Confirming server-side: the app asks your server

The app never calls the ChariPay API, not even to ask for a status. It asks your server for the order status, and your server knows it from the signed webhook:

  1. 1When it comes back from the hosted page, the app shows "verifying your payment".
  2. 2It queries your server about the order, at short intervals and for a limited time.
  3. 3Your server answers "paid" as soon as it has received, verified and stored the payment.succeeded webhook for that order, found through your metadata; for a session, the event also carries your orderId.
  4. 4If nothing has arrived, your server can itself read GET /v1/payment-sessions/{sessionId}.

Polling by the server is a fallback, not the source of truth. After downtime, reconcile with GET /v1/transactions. The receiver itself (signature over the raw body, anti-replay window, deduplication on Chari-Event-Id) is covered in getting your webhook integration right.

On the user experience side, plan for the user leaving the app before confirmation: when they reopen it, the app asks the server for the order status again, and the server may have received the webhook in the meantime.

What if your app shows its own card form?

It is possible, with direct checkout. Direct checkout lets you build your own card form: session verification, card submission, 3-D Secure return, payment status. These calls are not authenticated by your API key but by the session and its single-use verify key (vk) — never send your API key there — and they put you inside the card-data compliance scope.

In an app, the app itself does not make these calls. In direct checkout, the /checkout/* calls are made by your server, server-to-server; on the hosted checkout, ChariPay's payment page makes them itself. In both cases they are authenticated by the session and its vk, never by your API key. Your app's form therefore sends the card to your server, which submits the payment: card data goes through your app and your server.

Compare with the hosted page: your customer enters their card on the hosted checkout, within a PCI DSS Level 1 certified scope: you never see a card number, and your self-assessment questionnaire is reduced accordingly — to be confirmed with your advisor or your acquirer.

In an app, direct checkout only makes sense if you have a specific reason not to use the hosted page. The four calls, vk authentication and the decision grid are in the article on direct checkout.

Testing the mobile flow in test mode

Before you submit a release, run through this list on a real phone:

  • Key: chari_sk_test_… on the server only; search the app binary and the app repository for it: it must be in neither.
  • Return URLs: accept and decline are public https:// URLs that actually bring the user back on your target devices.
  • Successful payment: test card 4918 9141 0719 5005, CVV 123, any future expiry date, 3-D Secure code 555; only this card is accepted, and no real money moves.
  • Confirmation: the app shows "paid" only after your server has received the webhook; in the sandbox, webhooks are sent normally.
  • Abandonment: close the page mid-payment and check that the order stays "pending".
  • Double tap: a user who taps "Pay" twice must not create two sessions; send a stable Idempotency-Key per order, or a unique externalId: creating with an existing externalId returns the existing session as 200 OK instead of 201 Created.

Frequently asked questions

Do I need an SDK to accept payments in a mobile app?

No. The documented integration requires no mobile SDK: it goes through your server, which calls the REST API, and through the hosted checkout, which the app opens at the address it receives. Nothing ships inside the binary, and the same server handles your iOS and Android apps as well as your website. The API documentation also offers a Postman collection for your tests.

Can the app call the payment API directly?

No. API calls are authenticated by your key, and a key is a secret: never in the browser, a mobile app, a git repository or a screenshot. A published app can be decompiled. The app talks to your server; only your server talks to the ChariPay API, and it returns nothing to the app but the session's checkoutUrl.

Do I need a server to take payments in a mobile app?

For an API integration, yes: the server keeps the key, creates sessions and receives the signed webhooks. Without a server, there is no integration: you can send a customer a payment link created in the portal, which is single-use, one link per sale, and track the payment in the portal. That is a stopgap to get started, not an integration tied to your orders.

How does the app know the payment succeeded?

It asks your server. Your server knows from the signed payment.succeeded webhook, which is authoritative; the redirect to the accept URL only says the user came back. As a fallback, your server can read the session with GET /v1/payment-sessions/{sessionId}. The app shows "verifying your payment" until the server has confirmed it.

Where should I keep the test key during development?

On your development server, in an environment variable or a secrets vault, never in the app's code or its repository. The chari_sk_test_… test key moves no real money, but it gives access to your sandbox: treat it as a secret, and keep two sets of variables on the server, one for the sandbox and one for production.

Can the app show its own card form?

Yes, with direct checkout: the app's form sends the card to your server, and your server makes the /checkout/* calls, server-to-server, authenticated by the session and its vk, never by your API key. But it puts you inside the card-data compliance scope. It only makes sense if you have a specific reason not to use the hosted page, and the article on direct checkout details the trade-offs.

Does the card number go through my app?

With the hosted page, no form in your app collects the card: your customer enters it on the hosted checkout, within a PCI DSS Level 1 certified scope, and you never see a card number, neither in the app nor on your server. With direct checkout, on the other hand, card data goes through your app and your server, with the compliance obligations that come with it.

Next step

Set up your test server, register your webhook endpoint, then run the full flow from the app with the test card: Start in test mode. When you go live, in your code, you change only the key and the webhook secret: URLs, payloads and error codes are identical. To go further, read checkout session expiry and return and protecting your API keys.

Written by ChariPay team.

Read next

Compliance5 min read

Protecting your API keys

Protecting your payment API keys: five simple rules, planned rotation, and what to do, in order, when a key has already leaked into a repository.

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