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.

Base URL

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.

Authenticated request
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 response
{
  "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.

The ticket object
{
  "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.
POST /v1/tickets
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.

GET /v1/tickets/{id}
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.
POST /v1/tickets/{id}/call
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.

POST /v1/tickets/{id}/serve
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.

POST /v1/tickets/{id}/cancel
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.
GET /v1/branches/{id}/queue
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.

GET /v1/branches
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.
Delivery
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.

Verify a delivery
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);
});