Get started
Quickstart
Create a store, get a test key, take a first test payment and verify it, in about ten minutes.
This guide takes you from nothing to a verified test payment. You will need a Raïs business account and a server that can run a few lines of code. No money moves in the sandbox.
1. Get a test key
A business account is created with one store, which can use the sandbox straight away. In the dashboard, open Developers, then API keys, press Create key, and choose your store and the Test mode in the dialog. A test key starts with rais_sk_test_ and is shown once: copy it into your environment now.
export RAIS_SECRET_KEY="rais_sk_test_..."2. Check the key
Retrieving your account is the simplest call: it works with any valid key and tells you which store and environment the key belongs to.
curl https://api.rais.app/v1/account \
-H "Authorization: Bearer rais_sk_test_..."{
"object": "account",
"id": "7d0b6a3e-1c52-4f8e-9a44-2b6e8f1d0c35",
"name": "Boutique Alger",
"livemode": false,
"live_enabled": false,
"currency": "DZD",
"payment_page": "rais"
}livemode: false confirms that this is a sandbox key.
3. Create a payment
Create the payment from your server, with the amount in centimes. 500000 is 5,000.00 DA. The Idempotency-Key header makes the request safe to repeat: if the connection drops and you send it again with the same key, you get the same payment back instead of a second one. See Idempotency and retries.
curl https://api.rais.app/v1/payments \
-H "Authorization: Bearer rais_sk_test_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: b7e1c1c6-6f0a-4d2f-9d8b-3a5c2f1e7a90" \
-d '{
"amount": 500000,
"currency": "DZD",
"description": "Order #1042",
"reference": "order-1042",
"customer": {
"name": "Amina Benali",
"email": "amina@example.dz",
"phone": "+213550000000"
},
"success_url": "https://shop.example/thanks",
"cancel_url": "https://shop.example/cart",
"metadata": {
"cart_id": "c_8812"
},
"expires_in": 3600
}'{
"id": "5b1c2f4e-9d3a-4c61-8a7e-0f2d6b9e1a33",
"object": "payment",
"status": "pending",
"amount": 500000,
"currency": "DZD",
"description": "Order #1042",
"reference": "order-1042",
"customer": {
"id": null,
"name": "Amina Benali",
"email": "amina@example.dz",
"phone": "+213550000000"
},
"items": [
{
"name": "Running shoes",
"description": null,
"quantity": 2,
"unit_amount": 250000
}
],
"metadata": {
"cart_id": "c_8812"
},
"livemode": false,
"payment_page": "rais",
"checkout_url": "https://pay.rais.app/pay/5b1c2f4e-9d3a-4c61-8a7e-0f2d6b9e1a33",
"success_url": "https://shop.example/thanks",
"cancel_url": "https://shop.example/cart",
"expires_at": 1793003600,
"created": 1793000000,
"paid_at": null,
"amount_refunded": 0,
"refunded": false,
"fees": null,
"net": null,
"receipt_url": null,
"failure": null,
"payment_method": null,
"processor": {
"name": "satim",
"order_id": "V721uPPfNNofVQAAABL3",
"approval_code": null,
"response_code": null
}
}The payment is pending. Keep its id and reference; you will need them to match the result to your order.
4. Send the payer to the checkout
Redirect the payer to checkout_url. It is a hosted page, in English, French or Arabic, where they enter their card and confirm with 3-D Secure. When they are done, a result page shows the outcome and a button that takes them to your success_url or cancel_url with the payment id and a status added to the address. Some payers close the tab right after paying and never come back to your site: the webhook of step 6 still reaches you, which is why it matters. See Hosted checkout and return URLs.
5. Pay with a test card
The checkout of a sandbox payment is marked "Test mode". Type SATIM's test card as you would a real one (the cards are in Test cards and sandbox). This one always succeeds:
| Card number | Expiry (MM/YY) | CVV2 | 3-D Secure password |
|---|---|---|---|
6280 5811 1000 7215 | 01/27 | 373 | 123456 |
The other cards reproduce declines, expired cards and wrong passwords: see Test cards and sandbox.
6. Receive the webhook
Webhooks tell your server what happened without waiting for the payer's browser. In the dashboard, open Developers, then Webhooks, add an endpoint, choose your store and the Test mode in the dialog, and choose the events to receive (payment.succeeded is enough for now). Copy its signing secret: it starts with whsec_.
export RAIS_WEBHOOK_SECRET="whsec_..."Two things to know while you develop:
- Raïs delivers from the internet, so your endpoint must be reachable from it. For a server on your computer, use a tunnel (ngrok, Cloudflare Tunnel and similar). Addresses such as
localhostand private networks are refused. - A sandbox endpoint may use
http://. A live endpoint must usehttps://.
The Send test event button of the dashboard sends a fake payment.succeeded event to your endpoint, which is the quickest way to check that it answers.
7. Verify and confirm
Every webhook carries a Rais-Signature header. Verify it against the raw body before you trust anything in the request, then answer with a 2xx status. When the payer comes back to your site, do not trust the address either: retrieve the payment and check its status.
The server below does all of it: it creates the payment and redirects, asks the API for the truth on the return page, and verifies the signature of the webhook.
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const API = "https://api.rais.app/v1";
const SECRET_KEY = process.env.RAIS_SECRET_KEY; // rais_sk_test_...
const WEBHOOK_SECRET = process.env.RAIS_WEBHOOK_SECRET; // whsec_...
/**
* rawBody: the request body exactly as received (Buffer or string), never re-serialised JSON.
* header: the value of the Rais-Signature header.
*/
export function verifySignature(rawBody, header, secret, toleranceSeconds = 300, now = Math.floor(Date.now() / 1000)) {
const pairs = header.split(",").map((part) => part.trim().split("="));
const timestamp = pairs.find(([key]) => key === "t")?.[1];
const signatures = pairs.filter(([key]) => key === "v1").map(([, value]) => value);
if (!/^\d+$/.test(timestamp ?? "") || signatures.length === 0) return false;
if (Math.abs(now - Number(timestamp)) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest();
return signatures.some((signature) => {
const received = Buffer.from(signature, "hex");
return received.length === expected.length && timingSafeEqual(received, expected);
});
}
async function rais(path, init = {}) {
const response = await fetch(`${API}${path}`, {
...init,
headers: { Authorization: `Bearer ${SECRET_KEY}`, "Content-Type": "application/json", ...init.headers },
});
return { ok: response.ok, body: await response.json() };
}
const app = express();
const orders = new Map(); // order id -> { paymentId, amount }. Use your database in production.
const processed = new Set(); // ids of the events already handled. Use your database too.
app.get("/", (req, res) => {
res.send('<form method="post" action="/checkout"><button>Pay 5,000.00 DA</button></form>');
});
// 1. Create the payment on your server, keep its id with the order, then send the payer to the hosted checkout.
app.post("/checkout", async (req, res) => {
const orderId = "1042"; // your own order
const amount = 500000; // 5,000.00 DA, in centimes
const { ok, body: payment } = await rais("/payments", {
method: "POST",
headers: { "Idempotency-Key": `order-${orderId}` }, // one payment per order, even if this handler runs twice
body: JSON.stringify({
amount,
description: `Order #${orderId}`,
reference: `order-${orderId}`,
success_url: "http://localhost:3000/thanks", // https:// is required for live payments
cancel_url: "http://localhost:3000/",
}),
});
if (!ok) return res.status(502).json(payment);
orders.set(orderId, { paymentId: payment.id, amount });
res.redirect(303, payment.checkout_url);
});
// 2. The payer comes back here. The address proves nothing: ask the API about the payment YOU created for this order.
app.get("/thanks", async (req, res) => {
const order = orders.get(String(req.query.reference ?? "").replace(/^order-/, ""));
if (!order) return res.status(404).send("Unknown order.");
const { ok, body: payment } = await rais(`/payments/${order.paymentId}`);
const paid = ok && payment.status === "paid" && payment.amount === order.amount;
res.send(paid ? "Thank you, we received your payment." : "Payment not completed.");
});
// 3. Raïs calls this URL for every event. express.raw keeps the body exactly as it was signed.
app.post("/webhooks/rais", express.raw({ type: "application/json" }), (req, res) => {
if (!verifySignature(req.body, req.get("Rais-Signature") ?? "", WEBHOOK_SECRET)) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
if (event.id.startsWith("evt_test_")) return res.sendStatus(200); // the dashboard's test event: never fulfil an order for it
if (processed.has(event.id)) return res.sendStatus(200); // delivered twice: nothing to do
processed.add(event.id);
if (event.type === "payment.succeeded") {
const payment = event.data.object;
const orderId = [...orders].find(([, order]) => order.paymentId === payment.id)?.[0];
if (orderId && payment.amount === orders.get(orderId).amount) console.log(`Order ${orderId} is paid (${payment.amount} centimes)`);
}
res.sendStatus(200); // answer quickly, do slow work afterwards
});
app.listen(3000, () => console.log("Listening on http://localhost:3000"));Install the dependencies, set the two variables above and run it:
npm install express
node server.mjsOpen http://localhost:3000, press the button, pay with the test card, and watch your terminal print the paid order.