Ringo POS API

Tvoj POS kreira tiket za preuzimanje kada primi porudžbinu i štampa QR kod na računu. Kada kuhinja završi, POS označava tiket kao spreman. Webhookovi obaveštavaju tvoje sisteme o tome šta se desilo.

Keys start with rk_test_ on the test environment and only touch test data. Access to live keys is given to integration partners after a short check.

Quick start

  1. In the dashboard, open Connect and create an API key. It is shown once; Ringo stores only its hash.
  2. Create a ticket with your order number as reference.
  3. Print or show qr_url as a QR code for the guest.
  4. When the order is done, call …/ready. Ringo can alert the guest’s phone, depending on the phone and its settings.
# base URL — your Ringo host
export RINGO=https://<ringo-host>/api/v1
export RINGO_KEY=rk_test_…   # from Connect → API keys
curl -s $RINGO/shop -H "Authorization: Bearer $RINGO_KEY"

Authentication

Send the key in the Authorization header on every request. Keys belong to one shop and can only see that shop’s tickets. Revoke a key in Connect and requests with it fail at once with 401 invalid_key. Never put keys in a browser or an app: call the API from your POS server.

Authorization: Bearer rk_test_<shop>_<secret>

The ticket object

{
  "id": "tk_…",
  "number": "042",            // the number the guest sees (counts per queue)
  "queue": "main",            // main, or q_… when the shop runs several queues
  "status": "preparing",      // waiting | preparing | ready | picked_up | cancelled
  "reference": "A-1093",      // your order id, or null
  "customer_name": "Lina",
  "note": null,
  "invoice": "A-1093",
  "source": "api",            // api | counter | qr
  "scanned": false,           // true once the guest opened the ticket
  "customer_coming": false,   // the guest tapped "I'm coming"
  "created_at": "2026-11-03T09:12:44.000Z",
  "ready_at": null,
  "updated_at": "2026-11-03T09:12:44.000Z",
  "qr_url": "https://…"        // until scanned, then null
}

Create a ticket

POST/api/v1/tickets

All fields are optional. reference: 1 to 32 characters (letters, digits, . _ -). A reference that is still open answers 409 reference_exists; once a ticket is finished its reference can be used again.

curl -s -X POST $RINGO/tickets \
  -H "Authorization: Bearer $RINGO_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-A-1093" \
  -d '{"reference":"A-1093","customer_name":"Lina","note":"no sugar"}'

Answers 201 with {"ok":true,"ticket":{…}}. Turn ticket.qr_url into a QR code on the receipt.

Print the QR on the receipt

Every ticket has its own qr_url. It opens exactly that order on the guest’s phone, so print one QR per receipt. Send Idempotency-Key = your POS order id: if the till retries, you get the same ticket back, never a second one.

Option A, your POS draws QR codes: put ticket.qr_url into the receipt template’s QR element. Option B, Ringo draws it: fetch a ready SVG with the same key.

GET/api/v1/tickets/{id}/qr.svg?size=384

GET/api/v1/tickets/by-reference/{reference}/qr.svg

Black on white, error correction M, a 4-module quiet zone included. size is the pixel width, 96 to 1200 (default 384). Once the guest has scanned the QR, or the ticket is picked up or cancelled, it answers 409 {"error":"qr_unavailable"}.

Size on paper: 30 to 38 mm wide on a 58 mm roll and about 40 mm on an 80 mm roll. Leave white space around it and don’t let the POS stretch or dither it. Test one print with a phone before going live.

POS without custom receipts: use the shop QR instead. GET /api/v1/shop gives join_url. Print it once as a counter sticker (Dashboard → QR code), or show it on a customer display. Guests scan it and get the next number; staff link it to the order in the dashboard.

Change the status

POST/api/v1/tickets/{id}/ready

POST/api/v1/tickets/by-reference/{reference}/ready

Also /picked_up and /cancel. Marking ready can alert the guest’s phone, depending on the phone and its settings. Repeating a call is safe and answers "changed": false. A transition that is not possible (for example ready after cancel) answers 409 invalid_status with the current status.

List and read

GET/api/v1/tickets?status=ready&limit=50

GET/api/v1/tickets/{id}

GET/api/v1/tickets/by-reference/{reference}

GET/api/v1/shop

Lists are newest first, limit 1 to 200 (default 50). /shop returns the shop name, its public join URL, open tickets and the next number.

Several queues

A shop can run up to 10 queues (for example counter and terrace), created in Settings → Queues. Every queue has its own numbers and its own QR code. Shops with one queue need no changes: everything goes to main.

  • GET /api/v1/shop lists them in queues: id, name, join_url, archived.
  • Create a ticket in a queue with "queue":"q_…" in the body or ?queue=q_…. Without it the ticket goes to main.
  • An unknown id answers 400 unknown_queue. New tickets in an archived queue answer 409 queue_archived; its open tickets can still be finished.

Idempotency

Send Idempotency-Key (8 to 64 characters, A–Z a–z 0–9 _ -) on POST requests. If your POS retries after a timeout, the same key returns the first answer with the header Idempotent-Replayed: true and creates nothing new. Reusing a key for a different request answers 422 idempotency_key_reused. Keys are remembered for the last 200 requests per shop.

Webhooks

Add an HTTPS endpoint in Connect. Ringo POSTs JSON events to it and retries failures after 10 s, 1 min, 5 min, 15 min, 1 h and 3 h. Endpoints must use https on port 443 with a public DNS name; IP addresses, local names and credentials in the URL are refused, and redirects are not followed.

Events: ticket.created, ticket.claimed (the guest scanned), order.linked, ticket.status_changed, and webhook.test from the Test button.

POST /your/endpoint
Content-Type: application/json
Ringo-Event: ticket.status_changed
Ringo-Delivery: dlv_…            // unique per event and endpoint: dedupe on it
Ringo-Signature: t=1793700000,v1=5f1c…

{
  "id": "evt_…",
  "type": "ticket.status_changed",
  "created": 1793700000,
  "data": {
    "ticket": { "id": "tk_…", "number": "042", "queue": "main",
                "status": "ready", "order_id": "A-1093", "source": "pos" },
    "previous_status": "preparing",
    "action": "ready"
  }
}

Verify signatures

v1 is the hex HMAC-SHA256 of "<t>.<raw body>" with your endpoint’s whsec_ secret. Compare in constant time and reject timestamps older than 5 minutes.

// Node.js
import crypto from 'node:crypto';
function verify(secret, header, rawBody) {
  const p = Object.fromEntries(header.split(',').map((x) => x.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
  const want = crypto.createHmac('sha256', secret).update(p.t + '.' + rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(p.v1 || ''));
}

Errors

Errors answer {"ok":false,"error":"code"}.

StatuserrorMeaning
400bad_requestA field is invalid; see field.
401invalid_keyMissing, wrong or revoked key.
404not_foundNo such ticket in this shop.
409reference_existsThat reference is still open.
409invalid_statusThe transition is not possible now.
400unknown_queueNo queue with that id in this shop.
409queue_archivedThe queue is archived; restore it in Settings → Queues.
409fullToo many open tickets; close finished ones.
413 / 415too_large / unsupported_media_typeBodies are JSON, at most 4 KB.
422idempotency_key_reusedSame key, different request.
429rate_limitedSlow down; see Retry-After.

Rate limits

120 requests per minute per key, and 300 per minute per IP address. Over the limit the API answers 429 with a Retry-After header in seconds.

Questions about an integration? Contact support.