# Hosted checkout and return URLs

> Send the payer to the hosted page, bring them back to your site or app, and confirm the result on your server.

Source: https://rais.app/docs/checkout

When you create a payment, Raïs returns a `checkout_url`. That address is a hosted payment page: you never handle card details. This guide describes what the payer sees, where they go afterwards and how to confirm the result.

## Where the page lives

The payer's pages live on `https://pay.rais.app`: `checkout_url` is `https://pay.rais.app/pay/{payment_id}`, and the `receipt_url` of a paid payment is `https://pay.rais.app/pay/{payment_id}/receipt.pdf`. The API lives on `https://api.rais.app` (every endpoint is under `https://api.rais.app/v1`): call it from your server, never from the payer's browser.

Use the `checkout_url` you are given as it is, and do not build it yourself. If your site or your app restricts where it may redirect, or which hosts an in-app browser may open, allow `https://pay.rais.app`.

## The hosted page

Redirect the payer to `checkout_url` with a normal HTTP redirect (status 303 after a POST) or a link. The page:

- shows your store name and logo (upload the logo in the dashboard, under the store's **Settings**; a store without one shows its initial), the amount with commas between the thousands and a dot before the decimals (`5,000.00 DA`), the description and your reference;
- is available in English, French and Arabic, with right-to-left layout for Arabic. Set the language with `language` when you create the payment;
- collects the card number, expiry, CVC and name, then runs 3-D Secure through SATIM;
- lets the payer leave through a **Back to** link that returns to your `cancel_url`;
- marks a sandbox payment with a "Test mode" banner. It never lists test cards (they are in [Test cards and sandbox](https://rais.app/docs/testing.md)), and a live payment never shows any test wording.

The page sets a strict content security policy and forbids being framed: it cannot be embedded in an `<iframe>`. Redirect the payer, or open the page in a browser tab or a secure in-app browser. See [Mobile and app integrations](https://rais.app/docs/mobile.md).

### Raïs page or SATIM page

`payment_page` chooses where the card is entered. `rais` is Raïs' own checkout, described above. `satim` sends the payer to SATIM's payment page; they come back to a Raïs result page that shows the outcome. Return URLs and parameters work the same way for both. When you omit it, your store's default applies.

## Return URLs

`success_url` and `cancel_url` say where the payer goes when they leave the checkout. Set them on each payment; a payment without them uses the defaults of your store, and with neither, the result page tells the payer they can close it.

| URL | Used when |
| --- | --- |
| `success_url` | The payment was paid. |
| `cancel_url` | The payment failed, was cancelled, or the payer pressed **Back to**. |

An address is accepted when it:

- starts with `https://`, in both environments, or `http://`, in the **sandbox only**;
- or uses your **own app scheme**, such as `myapp://pay/result`, in both environments (a scheme of 2 to 31 characters: letters, digits, `+`, `.` and `-`);
- has at most 512 characters, no spaces, no non-ASCII characters (percent-encode them), no backslashes and no credentials (`user:password@host`).

`javascript:`, `data:`, `file:`, `vbscript:`, `blob:`, `about:` and `ftp:` are never accepted. An invalid address answers `invalid_url` with the parameter name.

### What Raïs adds to the address

Raïs **merges** four parameters into your address and keeps everything else as you wrote it: your own query parameters, a fragment, and a custom scheme.

| Parameter | Value |
| --- | --- |
| `payment_id` | The id of the payment. |
| `status` | `paid`, `failed` or `cancelled`: what the payer's browser saw. `failed` does not always close the payment (see below). Never trust it on its own. |
| `reference` | Your `reference`, only when the payment has one. |
| `order_number` | The Raïs order number of the payer's attempt. Each new attempt on the same payment gets a new one: identify the payment with `payment_id`. |

A parameter of yours with one of these names is replaced, so an address cannot pre-fill the result.

Raïs also adds `lang` (`en`, `fr` or `ar`), the language the payer used on the checkout, so your page can keep it, unless your address already has a `lang` of its own. A payer who picked the light or dark theme on the checkout is sent back with `theme=light` or `theme=dark` too (on the **Return to** and **Back to** links, never when the theme is on Auto). Both are only a convenience for your page and say nothing about the payment.

| You set | The payer is sent to |
| --- | --- |
| `https://shop.example/thanks` | `https://shop.example/thanks?payment_id=5b1c2f4e-9d3a-4c61-8a7e-0f2d6b9e1a33&status=paid&reference=order-1042&order_number=RS12AB34CD` |
| `https://shop.example/thanks?cart=7#top` | `https://shop.example/thanks?cart=7&payment_id=5b1c2f4e-9d3a-4c61-8a7e-0f2d6b9e1a33&status=paid&reference=order-1042&order_number=RS12AB34CD#top` |
| `myapp://pay/result?x=1` | `myapp://pay/result?x=1&payment_id=5b1c2f4e-9d3a-4c61-8a7e-0f2d6b9e1a33&status=paid&reference=order-1042&order_number=RS12AB34CD` |

The older `orderId` parameter is no longer sent.

### `status=failed` does not always end the payment

`status` describes what the payer's browser saw. A payer who leaves the card form, or whose attempt did not go through, comes back with `status=failed` (or `cancelled` after pressing **Back to**) although the payment is still `pending`: they can open the payment page again, press **Try again** and pay a minute later. So do not cancel your order because of the address. Wait for the `payment.failed` or `payment.cancelled` event, for `expires_at`, or retrieve the payment and look at its `status`.

## What the payer sees afterwards

Whatever the outcome, the payer first lands on a Raïs result page in the language of the payment, and **stays on it**: the page never sends the payer anywhere by itself, so they can read the details of the payment. A button leads back to your site.

| Outcome | Behaviour |
| --- | --- |
| Paid, with a return URL | The page shows the payment details (including the **Raïs order ID**, never the name on the card) and a button labelled **Return to** your store name. There is no automatic redirect and no countdown. |
| Paid, without a return URL | The page says the payer can close it. |
| Failed or cancelled | The page shows what happened, with no order ID. A **Try again** button leads back to the payment page for as long as the payment can still be paid, and a **Return to** button leads to your `cancel_url` when there is one. |
| Opened again later | A paid payment shows its paid page again. A refunded one shows "Payment refunded". An expired one shows an expired page. |

A paid result page shows the payment method as **CIB/Edahabia** and never a card number. It also offers **Download receipt (PDF)**, and on one row **Print** and **Email receipt**: the receipt goes to the payer's e-mail address when the payment has one, otherwise the payer types one; a receipt is e-mailed once per payment. See [Receipts](https://rais.app/docs/receipts.md).

## Never trust the redirect

The address the payer returns to can be opened by anybody, and the parameters in it can be typed by hand. `status=paid` proves nothing. And the redirect may never happen: a payer who pays and closes the tab never reaches your site, and Raïs still settles the payment in the background and sends `payment.succeeded` (see [Payments](https://rais.app/docs/payments.md#when-the-payer-pays-and-never-comes-back)). Before you ship anything, confirm the payment from your server, with one of these:

1. **The webhook.** A signed `payment.succeeded` event is the best trigger. See [Webhooks](https://rais.app/docs/webhooks.md).
2. **The API.** Retrieve the payment by the `payment_id` you stored when you created it, and check `status`, `amount` and `reference` against your order.

~~~javascript
// GET /thanks?payment_id=...&status=paid
const stored = await db.orders.findByPaymentId(req.query.payment_id); // the id you saved at creation
const response = await fetch(`https://api.rais.app/v1/payments/${stored.paymentId}`, {
  headers: { Authorization: `Bearer ${process.env.RAIS_SECRET_KEY}` },
});
const payment = await response.json();

if (payment.status === "paid" && payment.amount === stored.amount) {
  // fulfil the order, once
}
~~~

Use the id you saved, not the one in the address, and make fulfilment idempotent: the webhook and the return page may both arrive.


> [!TIP]
> **Pending is not failure**
>
> If the payer is back before the result reached you, `status` can still be `pending`. Show a "we are confirming your payment" page and wait for the webhook rather than treating it as a failure.


## Expired and unavailable payments

A payment that passed `expires_at` unpaid, was cancelled, or was declined by the bank opens a page that says it can no longer be paid (with a **Contact the merchant** button), and cannot be paid. A payment of a suspended store shows an unavailable page. In every case, create a new payment when the payer wants to try again.

A payment link stays payable until its own `expires_at`, however long the payer takes: if the gateway's own payment session ran out meanwhile, the checkout opens a new one by itself when the payer starts paying. A payment without an expiry, such as a link created in the dashboard with "Never", is never closed because of its age.
