# Raïs API documentation > Raïs is a payment platform for Algeria. Businesses accept CIB and Edahabia card payments through a REST API, a hosted checkout and webhooks; amounts are in Algerian dinars (DZD), always integers in centimes. This file contains every page of the documentation. The index is at https://rais.app/llms.txt. --- # Introduction > What Raïs is, how a payment travels from your server to the bank and back, and the concepts you will meet. Source: https://rais.app/docs Raïs lets a business in Algeria accept CIB and Edahabia card payments in Algerian dinars. You create a payment from your server, send the payer to a hosted checkout, and learn the outcome through a signed webhook or the API. Raïs handles the card form, 3-D Secure with SATIM, receipts and refunds. ## How a payment works A payment, end to end. Actors: Your server, Payer, Raïs, SATIM. 1. Your server to Raïs: POST /v1/payments 2. Raïs to Your server: payment with checkout_url 3. Your server to Payer: Redirect to checkout_url 4. Payer to Raïs: Enters the card on the hosted page 5. Raïs to SATIM: Authorisation and 3-D Secure 6. SATIM to Raïs: Approved or declined 7. Raïs to Payer: Redirect to success_url or cancel_url 8. Raïs to Your server: Webhook payment.succeeded (signed) 9. Your server to Raïs: GET /v1/payments/{id} to confirm 1. Your server creates a **payment** with the amount and your own reference. Raïs answers with a `checkout_url`. 2. You redirect the payer to that URL. They enter their card on the hosted page and confirm with 3-D Secure. 3. The result page shows the payer the outcome and a button back to your `success_url` or `cancel_url`; independently, Raïs delivers a signed **webhook** to your server. 4. Your server confirms the result (from the webhook, or by retrieving the payment) before it ships anything. The browser redirect is only a convenience for the payer. The webhook and the API are the source of truth. ## Concepts ### Stores A **store** is the merchant account that takes payments. It has a name, return URLs, branding, API keys and webhook endpoints. A business account starts with one store and can have several; every API key belongs to exactly one store, and a key only ever sees the payments of its own store. ### API keys You call the API with a **secret key** sent as a bearer token. Sandbox keys start with `rais_sk_test_`, live keys with `rais_sk_live_`. Keys belong on your server: the API sends no CORS headers on purpose, so a browser or a mobile app cannot use one. See [Authentication](https://rais.app/docs/authentication.md). ### Test and live The sandbox and live environments have the same API, the same URLs and the same objects. The **key decides** which environment a request belongs to. Sandbox payments are paid with SATIM's published test cards and never move money; live payments are real and need a verified business. See [Test cards and sandbox](https://rais.app/docs/testing.md) and [Going live](https://rais.app/docs/going-live.md). ### Payments A payment is one attempt to collect an amount from a payer. Amounts are **integers in centimes**: `500000` is 5,000.00 DA. The only currency is `DZD` and the smallest payment is 50.00 DA. A payment is `pending` until it is paid, and ends `paid`, `failed`, `cancelled` or `expired`. See [Payments](https://rais.app/docs/payments.md). ### Refunds You can refund all or part of a paid payment, as many times as you like, never beyond what was paid. See [Refunds](https://rais.app/docs/refunds.md). ### Pricing A paid payment costs SATIM's fee plus Raïs's, a refund costs a flat 10.00 DA, and every price excludes VAT. A payment carries its `fees` and its `net`, a refund its own `fees`. See [Pricing and fees](https://rais.app/docs/pricing.md). ### Events and webhooks Every change that matters creates an **event** such as `payment.succeeded`. Raïs delivers events to your webhook endpoints, signed, at least once, and keeps them readable through the API for 30 days. See [Webhooks](https://rais.app/docs/webhooks.md). ### Receipts Every paid payment has a PDF receipt for the payer, available through a link on the payment and through a signed link you can request. See [Receipts](https://rais.app/docs/receipts.md). ## Conventions | | | | --- | --- | | Base URL | `https://api.rais.app/v1` | | Payer's pages | `https://pay.rais.app`: the hosted checkout and the receipts (`checkout_url`, `receipt_url`) | | Format | JSON requests and responses, UTF-8 | | Authentication | `Authorization: Bearer rais_sk_...` | | Amounts | Integers in centimes (1 DA = 100) | | Timestamps | Unix time in seconds | | Ids | Payments, refunds and stores are UUIDs; events start with `evt_` | | Errors | One envelope for every error, with a stable `code` and a `request_id` | | Version | `2026-10-26`, in the `Rais-Version` header of every response | ## Where to go next - [Quickstart](https://rais.app/docs/quickstart.md): create a test key, take a first payment and verify it. - [Authentication](https://rais.app/docs/authentication.md): keys, scopes, rotation and IP allowlists. - [Payments](https://rais.app/docs/payments.md): the payment object, statuses and everything you can set. - [Webhooks](https://rais.app/docs/webhooks.md): receive and verify events. - [API reference](https://rais.app/docs/api.md): every endpoint with examples in nine languages. - [Use these docs with AI](https://rais.app/docs/ai-tools.md): llms.txt, Markdown pages and the OpenAPI file. --- # Quickstart > Create a store, get a test key, take a first test payment and verify it, in about ten minutes. Source: https://rais.app/docs/quickstart 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. ~~~bash export RAIS_SECRET_KEY="rais_sk_test_..." ~~~ > [!WARNING] > **Keep the key on your server** > > Anybody who holds a secret key can create payments in your name. Never put it in a web page, a mobile app or a repository. See [Authentication](https://rais.app/docs/authentication.md). ## 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. **Request** ```bash curl https://api.rais.app/v1/account \ -H "Authorization: Bearer rais_sk_test_..." ``` **Response** (`200`) ~~~json { "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](https://rais.app/docs/idempotency.md). **Request** ```bash 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 }' ``` **Response** (`201`) ~~~json { "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](https://rais.app/docs/checkout.md). ## 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](https://rais.app/docs/testing.md)). 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](https://rais.app/docs/testing.md). ## 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_`. ~~~bash 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 `localhost` and private networks are refused. - A sandbox endpoint may use `http://`. A live endpoint must use `https://`. 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. **A complete server: create a payment, redirect, receive the webhook** **Node.js** ```javascript 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('
'); }); // 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")); ``` **Python** ```python import os import requests from flask import Flask, abort, redirect, request import hashlib import hmac import time API = "https://api.rais.app/v1" SECRET_KEY = os.environ["RAIS_SECRET_KEY"] # rais_sk_test_... WEBHOOK_SECRET = os.environ["RAIS_WEBHOOK_SECRET"] # whsec_... AUTH = {"Authorization": f"Bearer {SECRET_KEY}"} app = Flask(__name__) orders = {} # order id -> {"payment_id": ..., "amount": ...}. Use your database in production. processed = set() # ids of the events already handled. Use your database too. def verify_signature(raw_body, header, secret, tolerance=300, now=None): """raw_body: the request body exactly as received (bytes). header: the Rais-Signature header.""" timestamp, signatures = None, [] for part in header.split(","): key, _, value = part.strip().partition("=") if key == "t": timestamp = value elif key == "v1": signatures.append(value) if timestamp is None or not timestamp.isascii() or not timestamp.isdigit() or not signatures: return False if abs((time.time() if now is None else now) - int(timestamp)) > tolerance: return False signed = timestamp.encode() + b"." + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest().encode() return any(hmac.compare_digest(expected, signature.lower().encode()) for signature in signatures) @app.get("/") def home(): return '' # 1. Create the payment on your server, keep its id with the order, then send the payer to the hosted checkout. @app.post("/checkout") def checkout(): order_id = "1042" # your own order amount = 500000 # 5,000.00 DA, in centimes response = requests.post( f"{API}/payments", headers={**AUTH, "Idempotency-Key": f"order-{order_id}"}, # one payment per order json={ "amount": amount, "description": f"Order #{order_id}", "reference": f"order-{order_id}", "success_url": "http://localhost:3000/thanks", # https:// is required for live payments "cancel_url": "http://localhost:3000/", }, timeout=30, ) if not response.ok: abort(502) payment = response.json() orders[order_id] = {"payment_id": payment["id"], "amount": amount} return redirect(payment["checkout_url"], code=303) # 2. The payer comes back here. The address proves nothing: ask the API about the payment YOU created for this order. @app.get("/thanks") def thanks(): order = orders.get(request.args.get("reference", "").removeprefix("order-")) if order is None: abort(404) payment = requests.get(f"{API}/payments/{order['payment_id']}", headers=AUTH, timeout=30).json() paid = payment.get("status") == "paid" and payment.get("amount") == order["amount"] return "Thank you, we received your payment." if paid else "Payment not completed." # 3. Raïs calls this URL for every event. get_data() returns the body exactly as it was signed. @app.post("/webhooks/rais") def webhook(): raw_body = request.get_data() if not verify_signature(raw_body, request.headers.get("Rais-Signature", ""), WEBHOOK_SECRET): abort(400) event = request.get_json(force=True) if event["id"].startswith("evt_test_"): return "", 200 # the dashboard's test event: never fulfil an order for it if event["id"] in processed: return "", 200 # delivered twice: nothing to do processed.add(event["id"]) if event["type"] == "payment.succeeded": payment = event["data"]["object"] order_id = next((key for key, order in orders.items() if order["payment_id"] == payment["id"]), None) if order_id and payment["amount"] == orders[order_id]["amount"]: print(f"Order {order_id} is paid ({payment['amount']} centimes)", flush=True) return "", 200 # answer quickly, do slow work afterwards if __name__ == "__main__": app.run(port=3000) ``` **PHP** ```php $tolerance) { return false; } $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret); $valid = false; foreach ($signatures as $signature) { $valid = hash_equals($expected, strtolower($signature)) || $valid; } return $valid; } function rais(string $method, string $path, ?array $body = null, array $extraHeaders = []): array { global $secretKey; $ch = curl_init(API . $path); $options = [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => array_merge(['Authorization: Bearer ' . $secretKey, 'Content-Type: application/json'], $extraHeaders), ]; if ($body !== null) { $options[CURLOPT_POSTFIELDS] = json_encode($body); } curl_setopt_array($ch, $options); $response = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); return [$status, json_decode((string) $response, true) ?? []]; } // order id => ['payment_id' => ..., 'amount' => ...] function orders(): array { return is_file(ORDERS_FILE) ? (json_decode(file_get_contents(ORDERS_FILE), true) ?? []) : []; } function saveOrder(string $orderId, array $order): void { file_put_contents(ORDERS_FILE, json_encode([$orderId => $order] + orders()), LOCK_EX); } $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); $method = $_SERVER['REQUEST_METHOD']; if ($path === '/' && $method === 'GET') { echo ''; } elseif ($path === '/checkout' && $method === 'POST') { // 1. Create the payment on your server, keep its id with the order, then send the payer to the hosted checkout. $orderId = '1042'; // your own order $amount = 500000; // 5,000.00 DA, in centimes [$status, $payment] = rais('POST', '/payments', [ 'amount' => $amount, 'description' => "Order #$orderId", 'reference' => "order-$orderId", 'success_url' => 'http://localhost:3000/thanks', // https:// is required for live payments 'cancel_url' => 'http://localhost:3000/', ], ["Idempotency-Key: order-$orderId"]); // one payment per order if ($status !== 201) { http_response_code(502); exit('Could not create the payment'); } saveOrder($orderId, ['payment_id' => $payment['id'], 'amount' => $amount]); header('Location: ' . $payment['checkout_url'], true, 303); } elseif ($path === '/thanks' && $method === 'GET') { // 2. The payer comes back here. The address proves nothing: ask the API about the payment YOU created for this order. $order = orders()[preg_replace('/^order-/', '', $_GET['reference'] ?? '')] ?? null; if ($order === null) { http_response_code(404); exit('Unknown order.'); } [$status, $payment] = rais('GET', '/payments/' . rawurlencode($order['payment_id'])); $paid = $status === 200 && ($payment['status'] ?? '') === 'paid' && ($payment['amount'] ?? 0) === $order['amount']; echo $paid ? 'Thank you, we received your payment.' : 'Payment not completed.'; } elseif ($path === '/webhooks/rais' && $method === 'POST') { // 3. Raïs calls this URL for every event. php://input is the body exactly as it was signed. $rawBody = file_get_contents('php://input'); if (!verifySignature($rawBody, $_SERVER['HTTP_RAIS_SIGNATURE'] ?? '', $webhookSecret)) { http_response_code(400); exit; } $event = json_decode($rawBody, true); if (str_starts_with($event['id'], 'evt_test_')) { http_response_code(200); // the dashboard's test event: never fulfil an order for it exit; } // Delivered twice? Keep $event['id'] in your database and skip the ones you have seen. if ($event['type'] === 'payment.succeeded') { $payment = $event['data']['object']; foreach (orders() as $orderId => $order) { if ($order['payment_id'] === $payment['id'] && $order['amount'] === $payment['amount']) { error_log("Order $orderId is paid ({$payment['amount']} centimes)"); } } } http_response_code(200); // answer quickly, do slow work afterwards } else { http_response_code(404); } ``` **Go** ```go package main import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "log" "math" "net/http" "net/url" "os" "strconv" "strings" "sync" "time" ) type order struct { PaymentID string Amount int } var ( api = "https://api.rais.app/v1" secretKey = os.Getenv("RAIS_SECRET_KEY") // rais_sk_test_... webhookSecret = os.Getenv("RAIS_WEBHOOK_SECRET") // whsec_... orders sync.Map // order id -> order. Use your database in production. processed sync.Map // ids of the events already handled. Use your database too. ) // VerifySignature checks a Rais-Signature header against the raw request body. func VerifySignature(rawBody []byte, header, secret string, tolerance time.Duration, now time.Time) bool { var timestamp string var signatures []string for _, part := range strings.Split(header, ",") { key, value, _ := strings.Cut(strings.TrimSpace(part), "=") switch key { case "t": timestamp = value case "v1": signatures = append(signatures, value) } } if timestamp == "" || strings.Trim(timestamp, "0123456789") != "" || len(signatures) == 0 { return false } seconds, err := strconv.ParseInt(timestamp, 10, 64) if err != nil || math.Abs(float64(now.Unix()-seconds)) > tolerance.Seconds() { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp + ".")) mac.Write(rawBody) expected := mac.Sum(nil) valid := false for _, signature := range signatures { if received, err := hex.DecodeString(signature); err == nil && hmac.Equal(received, expected) { valid = true } } return valid } func rais(method, path string, body any, headers map[string]string) (int, map[string]any, error) { var reader io.Reader if body != nil { encoded, err := json.Marshal(body) if err != nil { return 0, nil, err } reader = bytes.NewReader(encoded) } req, err := http.NewRequest(method, api+path, reader) if err != nil { return 0, nil, err } req.Header.Set("Authorization", "Bearer "+secretKey) req.Header.Set("Content-Type", "application/json") for name, value := range headers { req.Header.Set(name, value) } res, err := http.DefaultClient.Do(req) if err != nil { return 0, nil, err } defer res.Body.Close() var decoded map[string]any _ = json.NewDecoder(res.Body).Decode(&decoded) return res.StatusCode, decoded, nil } func main() { mux := http.NewServeMux() mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, ``) }) // 1. Create the payment on your server, keep its id with the order, then send the payer to the hosted checkout. mux.HandleFunc("POST /checkout", func(w http.ResponseWriter, r *http.Request) { orderID := "1042" // your own order amount := 500000 // 5,000.00 DA, in centimes status, payment, err := rais("POST", "/payments", map[string]any{ "amount": amount, "description": "Order #" + orderID, "reference": "order-" + orderID, "success_url": "http://localhost:3000/thanks", // https:// is required for live payments "cancel_url": "http://localhost:3000/", }, map[string]string{"Idempotency-Key": "order-" + orderID}) // one payment per order if err != nil || status != http.StatusCreated { http.Error(w, "could not create the payment", http.StatusBadGateway) return } orders.Store(orderID, order{PaymentID: payment["id"].(string), Amount: amount}) http.Redirect(w, r, payment["checkout_url"].(string), http.StatusSeeOther) }) // 2. The payer comes back here. The address proves nothing: ask the API about the payment YOU created for this order. mux.HandleFunc("GET /thanks", func(w http.ResponseWriter, r *http.Request) { value, found := orders.Load(strings.TrimPrefix(r.URL.Query().Get("reference"), "order-")) if !found { http.NotFound(w, r) return } known := value.(order) status, payment, err := rais("GET", "/payments/"+url.PathEscape(known.PaymentID), nil, nil) if err == nil && status == http.StatusOK && payment["status"] == "paid" && payment["amount"] == float64(known.Amount) { fmt.Fprint(w, "Thank you, we received your payment.") return } fmt.Fprint(w, "Payment not completed.") }) // 3. Raïs calls this URL for every event. ReadAll returns the body exactly as it was signed. mux.HandleFunc("POST /webhooks/rais", func(w http.ResponseWriter, r *http.Request) { rawBody, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) if err != nil || !VerifySignature(rawBody, r.Header.Get("Rais-Signature"), webhookSecret, 300*time.Second, time.Now()) { w.WriteHeader(http.StatusBadRequest) return } var event struct { ID string `json:"id"` Type string `json:"type"` Data struct { Object struct { ID string `json:"id"` Amount int `json:"amount"` } `json:"object"` } `json:"data"` } if err := json.Unmarshal(rawBody, &event); err != nil { w.WriteHeader(http.StatusBadRequest) return } if strings.HasPrefix(event.ID, "evt_test_") { w.WriteHeader(http.StatusOK) // the dashboard's test event: never fulfil an order for it return } if _, seen := processed.LoadOrStore(event.ID, true); seen { w.WriteHeader(http.StatusOK) // delivered twice: nothing to do return } if event.Type == "payment.succeeded" { orders.Range(func(orderID, value any) bool { if known := value.(order); known.PaymentID == event.Data.Object.ID && known.Amount == event.Data.Object.Amount { log.Printf("Order %s is paid (%d centimes)", orderID, known.Amount) } return true }) } w.WriteHeader(http.StatusOK) // answer quickly, do slow work afterwards }) log.Println("Listening on http://localhost:3000") log.Fatal(http.ListenAndServe(":3000", mux)) } ``` Install the dependencies, set the two variables above and run it: **Node.js** ~~~bash npm install express node server.mjs ~~~ **Python** ~~~bash pip install flask requests python app.py ~~~ **PHP** ~~~bash php -S localhost:3000 index.php ~~~ **Go** ~~~bash go run main.go ~~~ Open `http://localhost:3000`, press the button, pay with the test card, and watch your terminal print the paid order. ## Next steps - [Webhooks](https://rais.app/docs/webhooks.md): events, retries, secret rotation and the verification code in every language. - [Payments](https://rais.app/docs/payments.md): statuses, metadata, items, expiry and cancellation. - [Going live](https://rais.app/docs/going-live.md): business verification and the checklist before real payments. --- # Authentication > Secret API keys, the test and live environments, scopes, rotation and IP allowlists. Source: https://rais.app/docs/authentication Every request to the API carries a **secret key** as a bearer token. A key belongs to one store and one environment, can be limited to some operations, and can be restricted to the addresses your servers use. ## Send the key **Request** ```bash curl https://api.rais.app/v1/account \ -H "Authorization: Bearer rais_sk_test_..." ``` The header is `Authorization: Bearer