# 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('<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"));
```

**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 '<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")
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
<?php
// Run with: php -S localhost:3000 index.php
const API = 'https://api.rais.app/v1';
const ORDERS_FILE = __DIR__ . '/orders.json'; // use your database in production
$secretKey = getenv('RAIS_SECRET_KEY'); // rais_sk_test_...
$webhookSecret = getenv('RAIS_WEBHOOK_SECRET'); // whsec_...

function verifySignature(string $rawBody, string $header, string $secret, int $tolerance = 300, ?int $now = null): bool
{
    $timestamp = null;
    $signatures = [];
    foreach (explode(',', $header) as $part) {
        [$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
        if ($key === 't') {
            $timestamp = $value;
        } elseif ($key === 'v1') {
            $signatures[] = $value;
        }
    }
    if ($timestamp === null || !ctype_digit($timestamp) || $signatures === []) {
        return false;
    }
    if (abs(($now ?? time()) - (int) $timestamp) > $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 '<form method="post" action="/checkout"><button>Pay 5,000.00 DA</button></form>';
} 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, `<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.
	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.
