API reference
The Ranna API
Put people in your queue from a booking site, a kiosk or your POS, ring them when you're ready, and hear back when anything changes. JSON over HTTPS, one API key per integration.
Included in the Business and Enterprise plans. Up to 600 requests per minute per key.
https://ranna.tn/api/v1
Authentication
Create a key in the dashboard under API & webhooks. Keys start with rk_live_ and are shown once: we only keep a hash.
Send it as a Bearer token on every request, from your server only. Never ship a key in a browser or app bundle.
curl https://ranna.tn/api/v1/branches \
-H "Authorization: Bearer rk_live_…"Errors
Errors use standard HTTP status codes and a consistent body.
- 400
- The body or a parameter is invalid.
- 401
- Missing or revoked API key.
- 404
- The ID doesn’t exist in your business.
- 409
- A status change that isn’t allowed, such as serving a cancelled ticket.
- 429
- Too many requests. Wait for Retry-After, then retry.
{
"error": {
"code": "invalid_transition",
"message": "Cannot serve a ticket that is cancelled."
}
}The ticket object
Status moves from waiting to called, then ends as served, cancelled or no_show. Numbers restart at 1 each day per branch.
position, people_ahead and estimated_wait_seconds are null once a ticket leaves the waiting state. url is the live ticket page to send the customer.
{
"object": "ticket",
"id": "8f6a3c1e-2d4b-4c1a-9a57-0f3e6b8d2c11",
"code": "A47",
"number": 47,
"status": "waiting",
"branch_id": "b3c0f6a2-…",
"counter_id": null,
"source": "api",
"lang": "ar",
"customer_name": "Sami",
"customer_phone": "+216 20 123 456",
"external_id": "booking_1029",
"position": 3,
"people_ahead": 2,
"estimated_wait_seconds": 540,
"call_count": 0,
"url": "https://ranna.tn/t/8f6a3c1e-2d4b-4c1a-9a57-0f3e6b8d2c11",
"created_at": "2026-10-16T09:41:07.120Z",
"called_at": null,
"served_at": null,
"cancelled_at": null,
"no_show_at": null
}Create a ticket
POST /v1/tickets
Adds a customer to the end of a branch queue and returns the ticket, including the link to send them. Fires ticket.created.
Body parameters
branch_iduuid, required- The branch queue to join.
customer_namestring- Shown to staff only. Up to 80 characters.
customer_phonestring- Digits, spaces and a leading +. Used for WhatsApp alerts later.
lang"ar" | "fr" | "en"- Ticket page language. Defaults to ar. The customer can switch on the page.
external_idstring- Your own reference, echoed back in webhooks.
curl -X POST https://ranna.tn/api/v1/tickets \
-H "Authorization: Bearer $RANNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"branch_id": "b3c0f6a2-…",
"customer_name": "Sami",
"lang": "ar",
"external_id": "booking_1029"
}'Get a ticket
GET /v1/tickets/{id}
Returns the ticket with its live position, people ahead and estimated wait.
curl https://ranna.tn/api/v1/tickets/8f6a3c1e-… \
-H "Authorization: Bearer $RANNA_API_KEY"Call a ticket
POST /v1/tickets/{id}/call
Rings the customer's phone and puts the number on the TV. Calling an already-called ticket rings it again. Fires ticket.called.
Body parameters
counter_iduuid- Which counter the customer should go to.
curl -X POST https://ranna.tn/api/v1/tickets/8f6a3c1e-…/call \
-H "Authorization: Bearer $RANNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "counter_id": "c41d…" }'Mark served
POST /v1/tickets/{id}/serve
Closes the ticket as served. The customer's page shows a thank-you screen. Fires ticket.served.
curl -X POST https://ranna.tn/api/v1/tickets/8f6a3c1e-…/serve \
-H "Authorization: Bearer $RANNA_API_KEY"Cancel a ticket
POST /v1/tickets/{id}/cancel
Removes a waiting or called ticket from the queue. Fires ticket.cancelled.
curl -X POST https://ranna.tn/api/v1/tickets/8f6a3c1e-…/cancel \
-H "Authorization: Bearer $RANNA_API_KEY"List the queue
GET /v1/branches/{id}/queue
Returns the branch's tickets, oldest first. Defaults to waiting and called; filter with ?status=waiting,called,served,cancelled,no_show.
Query parameters
statuscomma list- Which statuses to include.
curl "https://ranna.tn/api/v1/branches/b3c0f6a2-…/queue?status=waiting" \
-H "Authorization: Bearer $RANNA_API_KEY"List branches
GET /v1/branches
Your branches and their counters, so you can find the IDs the other endpoints need.
curl https://ranna.tn/api/v1/branches \
-H "Authorization: Bearer $RANNA_API_KEY"Webhooks
Add an HTTPS endpoint in the dashboard and we'll POST an event whenever a ticket changes. Respond with any 2xx within 5 seconds. The data field is the full ticket object.
Failed deliveries (timeouts, network errors, 5xx, 408 and 429) are retried with exponential backoff for about ten minutes. The event id stays the same across retries, so you can ignore duplicates.
Event types
ticket.created- A ticket joined a queue, from a QR scan, the dashboard or the API.
ticket.called- Staff or the API called the ticket. Sent again on every re-ring.
ticket.served- The ticket was marked served.
ticket.cancelled- The customer left the queue or the ticket was cancelled.
POST /hooks/ranna HTTP/1.1
Content-Type: application/json
Ranna-Event: ticket.called
Ranna-Signature: t=1792143667,v1=5f2b9c…
{
"id": "evt_Qm3kX0aP9sLr2VbN7cTe",
"type": "ticket.called",
"created": 1792143667,
"data": { "object": "ticket", "id": "8f6a3c1e-…", "code": "A47", "status": "called", … }
}Verifying signatures
Every delivery carries Ranna-Signature: t=…,v1=…. Compute an HMAC-SHA256 of `${t}.${rawBody}` with your webhook secret and compare it to v1 in constant time.
Reject timestamps older than five minutes to stop replays.
import crypto from "node:crypto";
// Express example: use the raw body, not the parsed JSON.
app.post("/hooks/ranna", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("Ranna-Signature") ?? "";
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", process.env.RANNA_WEBHOOK_SECRET)
.update(`${t}.${req.body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
const valid = v1 && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
if (!fresh || !valid) return res.sendStatus(400);
const event = JSON.parse(req.body);
if (event.type === "ticket.called") {
// e.g. mark the booking as "at the counter"
}
res.sendStatus(200);
});