Ringo POS API
你的 POS 在收到订单时创建取餐订单票,并在小票上打印二维码。厨房完成后,POS 将订单票标记为已就绪。Webhook 会通知你的系统发生了什么。
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
- In the dashboard, open Connect and create an API key. It is shown once; Ringo stores only its hash.
- Create a ticket with your order number as
reference. - Print or show
qr_urlas a QR code for the guest. - 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/shoplists them inqueues: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 tomain. - An unknown id answers
400 unknown_queue. New tickets in an archived queue answer409 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"}.
| Status | error | Meaning |
|---|---|---|
| 400 | bad_request | A field is invalid; see field. |
| 401 | invalid_key | Missing, wrong or revoked key. |
| 404 | not_found | No such ticket in this shop. |
| 409 | reference_exists | That reference is still open. |
| 409 | invalid_status | The transition is not possible now. |
| 400 | unknown_queue | No queue with that id in this shop. |
| 409 | queue_archived | The queue is archived; restore it in Settings → Queues. |
| 409 | full | Too many open tickets; close finished ones. |
| 413 / 415 | too_large / unsupported_media_type | Bodies are JSON, at most 4 KB. |
| 422 | idempotency_key_reused | Same key, different request. |
| 429 | rate_limited | Slow 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.