Desenvolvedores

API do Scanly

Conecte um POS, um totem, um CRM ou seu próprio site aos clientes, cartões fidelidade, agendamentos e pedidos do estabelecimento. REST com JSON, webhooks e um documento OpenAPI.

https://scanly.app/api/v1OpenAPI 3.1

A referência está em inglês, assim como a própria API.

Overview

The Scanly API connects a place's own systems — a POS, a self-service kiosk, a CRM, a website — to its guests, loyalty cards, bookings and orders.

Authentication. Every request sends a secret key: Authorization: Bearer sk_live_…. Create keys in Dashboard → API (owners, Pro plan). sk_test_… keys work on a sandbox copy of the place: same menu, specialists and loyalty program, no real guests.

Money is always in minor units of the place's currency (1250 = 12.50 EUR), with currency next to it. Times are ISO 8601 in UTC; dates of the place (YYYY-MM-DD) are local to its timezone.

Idempotency. Send Idempotency-Key: <unique id> with writes: a retry with the same key gets the first response back (header Idempotent-Replayed: true) instead of charging points twice.

Errors use HTTP status codes and one JSON shape: {"error": {"type", "code", "message", "param", "request_id"}}.

Rate limits: 300 requests a minute per live key, 120 per test key (X-RateLimit-* headers; 429 with Retry-After).

Lists return {"object": "list", "data": [...], "has_more", "next_cursor"}; pass next_cursor as cursor for the next page.

Webhooks tell your server what happened, signed like Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature). Add endpoints in Dashboard → API.

Quick start

1. In the dashboard open API (owners, Pro plan) and create a test key. Test keys work on a sandbox copy of your place: the same menu, specialists and loyalty program, no real guests.

2. Call the API with the key as a bearer token:

curl https://scanly.app/api/v1/restaurant \
  -H "Authorization: Bearer sk_test_…"

3. Build your integration against the sandbox, then create a live key for production. Keep keys on your server — never in an app or a web page.

  • Money is in minor units of the place's currency: 1250 is 12.50 EUR.
  • Times are ISO 8601 in UTC; local dates (YYYY-MM-DD) are in the place's timezone.
  • Send an Idempotency-Key header with writes, so a retry after a timeout never runs twice.
  • Lists are pages: pass next_cursor as cursor while has_more is true.

Guias

Loyalty at the till (POS)

The guest shows their card (Apple or Google Wallet, the Scanly app, or a printed code). Scan or type the code; the API takes it in any case and with or without the dash.

1. Preview the bill — discount percentage, points available and to be earned:

curl -X POST https://scanly.app/api/v1/loyalty/cards/K7F3-QX2M/quote \
  -H "Authorization: Bearer $SCANLY_KEY" -H "Content-Type: application/json" \
  -d '{"amount": 5000}'

2. Spend points the guest wants to use (points cards). Fails with 409 insufficient_points and changes nothing if the balance is too low:

curl -X POST https://scanly.app/api/v1/loyalty/cards/K7F3-QX2M/redemptions \
  -H "Authorization: Bearer $SCANLY_KEY" -H "Idempotency-Key: R-1001-spend" \
  -H "Content-Type: application/json" \
  -d '{"type": "points", "amount": 1000, "reference": "R-1001"}'

3. Record the paid bill — after discounts and points. Points cards earn cashback; discount cards move up levels:

curl -X POST https://scanly.app/api/v1/loyalty/cards/K7F3-QX2M/purchases \
  -H "Authorization: Bearer $SCANLY_KEY" -H "Idempotency-Key: R-1001-buy" \
  -H "Content-Type: application/json" \
  -d '{"amount": 4000, "reference": "R-1001"}'

Refunds: reverse the purchase transaction with POST /loyalty/transactions/{id}/reverse: its spend and the points it earned are taken back. Stamp cards use POST /loyalty/cards/{card}/stamps and redemptions with type: "reward".

Guias

Sign a guest up (kiosk, website, CRM)

Create the guest. Phone, email and external_id are unique per place: a duplicate returns 409 customer_exists with details.customer_id, so you can link to the existing guest instead.

const res = await fetch("https://scanly.app/api/v1/customers", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.SCANLY_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Olga Petrova", phone: "+4915112345678", marketing_opt_in: true, external_id: "crm-42" }),
});
const body = await res.json();
const customerId = res.ok ? body.id : body.error.details?.customer_id;

Then issue a card and send the guest to https://scanly.app/c/{code}: from there they add it to Apple Wallet or Google Wallet.

curl -X POST https://scanly.app/api/v1/loyalty/cards \
  -H "Authorization: Bearer $SCANLY_KEY" -H "Content-Type: application/json" \
  -d '{"customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d"}'

Guias

Online booking from your own site

Read who works and the services (GET /catalog: items with duration_minutes are bookable), then ask for free start times of the chosen services:

curl "https://scanly.app/api/v1/availability?service_ids=8bc29f32-55fc-4714-afac-559714a24454&date=2026-11-03&days=7" \
  -H "Authorization: Bearer $SCANLY_KEY"

Book one of the times. With specialist_id: null the least busy free specialist gets it. The guest receives the confirmation and reminders like any Scanly booking:

import requests

r = requests.post(
    "https://scanly.app/api/v1/bookings",
    headers={"Authorization": f"Bearer {KEY}", "Idempotency-Key": "web-7781"},
    json={
        "service_ids": ["8bc29f32-55fc-4714-afac-559714a24454"],
        "specialist_id": None,
        "starts_at": "2026-11-03T09:00:00Z",
        "guest": {"name": "Olga Petrova", "phone": "+4915112345678", "email": "olga@example.com", "locale": "de"},
    },
)
if r.status_code == 409:
    print(r.json()["error"]["code"])  # slot_taken: show the guest other times

Keep your calendar in step with the booking.created, booking.updated and booking.cancelled webhooks — bookings also come from the Scanly page, the app and the dashboard.

Guias

Stop list and prices from the POS

When the kitchen runs out, mark the item sold out: guests see it greyed out and can't order it until 5:00 the next morning (the place's time).

curl -X PATCH https://scanly.app/api/v1/catalog/items/0d1c2b3a-4e5f-4a6b-8c7d-9e0f1a2b3c4d \
  -H "Authorization: Bearer $SCANLY_KEY" -H "Content-Type: application/json" \
  -d '{"sold_out": true}'

available: false hides an item; price changes it (minor units). Orders placed from tables arrive as order.created webhooks; move them through the kitchen with PATCH /orders/{id}.

Webhooks

Add an endpoint in Dashboard → API. Scanly POSTs JSON events to it as they happen: live endpoints get your place's events, test endpoints the sandbox's. Answer with any 2xx within 10 seconds; otherwise we retry with growing pauses for about two days (1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h). You can resend any delivery from the dashboard.

{
  "id": "evt_5b1f0c9e2a7d4c3b8e6f1a2d",
  "type": "loyalty.transaction",
  "created_at": "2026-11-03T09:05:12.000Z",
  "livemode": true,
  "data": { "object": "loyalty_transaction", "type": "purchase", "amount": 4000, "card": { … } }
}
  • booking.createdSomeone booked: online, in the app, from the dashboard or through the API.
  • booking.updatedConfirmed, moved, marked as came or didn't come.
  • booking.cancelledCancelled by the guest or the place (cancelled_by).
  • customer.createdA new guest in the list (booking, dashboard, API).
  • customer.updatedGuest details changed.
  • customer.deletedGuest erased (GDPR).
  • loyalty.card_createdA loyalty card was issued.
  • loyalty.transactionStamps, a purchase, spent points, a redeemed reward or a reversal — with the card's new balance.
  • order.createdA table order or an order ahead for pickup.
  • order.updatedThe order's status changed.

Every request is signed like Standard Webhooks: verify webhook-signature against your endpoint's secret before trusting it, and drop events whose webhook-id you have already handled (retries keep the id).

import crypto from "node:crypto";

// secret: "whsec_…" from the dashboard; body: the raw request body as a string
export function verify(secret, headers, body) {
  const id = headers["webhook-id"], timestamp = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // replayed
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto.createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest("base64");
  return headers["webhook-signature"].split(" ").some((s) => {
    const given = Buffer.from(s.replace(/^v1,/, ""));
    return given.length === expected.length && crypto.timingSafeEqual(given, Buffer.from(expected));
  });
}

Erros

  • 400 parameter_invalidA field is missing or wrong; param names it.
  • 400 invalid_jsonThe body isn't JSON.
  • 401 api_key_missing / api_key_invalidNo key, or an unknown or revoked one.
  • 402 plan_requiredThe place isn't on Pro.
  • 403 scope_missingThe key can't do this; details.required_scope says what it needs.
  • 404 resource_missingNot found — or it belongs to another place.
  • 409 customer_existsDuplicate phone, email or external_id; details.customer_id is the existing guest.
  • 409 insufficient_points / no_rewardThe card doesn't have it. Nothing changed.
  • 409 slot_taken / outside_hoursThe time isn't free. Ask /availability again.
  • 409 already_reversedThis transaction was reversed before.
  • 409 loyalty_not_set_upThe place has no loyalty program.
  • 409 request_in_progressSame Idempotency-Key, first request still running.
  • 422 idempotency_key_reusedSame Idempotency-Key with a different request.
  • 429 rate_limitedSlow down; retry after Retry-After seconds.
  • 500 internal_errorOur side. Retry; quote request_id to support.

Referência

Restaurant

The place your key belongs to.

GET/restaurant

Get the place

The place the key belongs to: currency, time zone, languages, links and its loyalty program. Handy as a first call to check a key.

Escopo: restaurant:read

curl https://scanly.app/api/v1/restaurant \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "restaurant",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "name": "Olga Petrova",
  "kind": "beauty",
  "currency": "EUR",
  "timezone": "Europe/Berlin",
  "locales": [
    "en"
  ],
  "address": "…",
  "menu_url": "https://studio-bloom.guests.menu",
  "booking_url": "https://studio-bloom.guests.place",
  "plan": "free",
  "livemode": true,
  "booking_enabled": false,
  "loyalty_program": {
    "type": "points",
    "enabled": false,
    "stamps_required": 1,
    "cashback_percent": 1,
    "discount_tiers": [
      {
        "min_spend": 1,
        "percent": 10
      }
    ]
  }
}

Referência

Catalog

The menu or price list: sections, dishes, drinks and services.

GET/catalog

Get the menu or price list

Every section with its items (dishes, drinks or services) in menu order, with prices, durations and what is sold out right now. Texts come in every language the place publishes.

Escopo: catalog:read

curl https://scanly.app/api/v1/catalog \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "catalog",
  "currency": "EUR",
  "sections": [
    {
      "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
      "name": {
        "en": "Women’s haircut"
      },
      "items": [
        {
          "object": "catalog_item",
          "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
          "section_id": "590f69b9-b696-4280-904b-cc824747a409",
          "name": {
            "en": "Women’s haircut"
          },
          "description": {
            "en": "Women’s haircut"
          },
          "price": 4500,
          "price_from": false,
          "duration_minutes": 60,
          "available": true,
          "sold_out": false,
          "image_url": null,
          "allergens": [
            "…"
          ],
          "tags": [
            "VIP"
          ]
        }
      ]
    }
  ]
}
PATCH/catalog/items/{id}

Update an item

Keep the menu in step with your POS: hide an item, put it on the stop list (sold out today) or change its price.

Escopo: catalog:write

Parâmetros

  • id*path · stringId

Corpo

  • availablebooleanfalse hides it from guests
  • sold_outbooleantrue puts it on the stop list until the next business day (5:00 local time)
  • priceinteger · nullableNew price in minor units; null hides the price
curl -X PATCH https://scanly.app/api/v1/catalog/items/4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "available": true,
  "sold_out": false,
  "price": 4500
}'
Resposta · 200
{
  "object": "catalog_item",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "section_id": "590f69b9-b696-4280-904b-cc824747a409",
  "name": {
    "en": "Women’s haircut"
  },
  "description": {
    "en": "Women’s haircut"
  },
  "price": 4500,
  "price_from": false,
  "duration_minutes": 60,
  "available": true,
  "sold_out": false,
  "image_url": null,
  "allergens": [
    "…"
  ],
  "tags": [
    "VIP"
  ]
}

Referência

Customers

The place's guest list: everyone who booked, holds a loyalty card or was added by you.

GET/customers

List or find guests

Newest first. Filter by phone, email or external_id to find one guest (exact match after normalising), or search names with q.

Escopo: customers:read

Parâmetros

  • limitquery · integerHow many to return, 1–100 (default 20).
  • cursorquery · stringnext_cursor of the previous page.
  • phonequery · string
  • emailquery · string
  • external_idquery · string
  • qquery · stringPart of a name, email or phone
curl https://scanly.app/api/v1/customers \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "list",
  "data": [
    {
      "object": "customer",
      "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
      "name": "Olga Petrova",
      "phone": "+4915112345678",
      "email": "olga@example.com",
      "birthday": "1990-05-17",
      "locale": "en",
      "marketing_opt_in": false,
      "note": "Allergic to latex gloves",
      "tags": [
        "VIP"
      ],
      "external_id": "pos-42",
      "created_at": "2026-10-29T12:00:00.000Z",
      "updated_at": "2026-10-29T12:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
POST/customers

Create a guest

Adds a guest to the place's guest list. Phone, email and external_id are unique per place: a duplicate answers 409 customer_exists with the existing guest's id in details.customer_id, so you can link to it instead.

Escopo: customers:write · Erros: customer_exists

Corpo

  • name*stringFull name
  • phonestring · nullableAny format; stored as +<country><number> when it has a country code
  • emailstring · email · nullable
  • birthdaystring · nullableYYYY-MM-DD, or MM-DD when the year is unknown
  • localestring · nullableLanguage for messages, e.g. en, de, ru
  • marketing_opt_inbooleanThe guest agreed to news and offers
  • notestring · nullable
  • tagsstring[]
  • external_idstring · nullableYour system's id; unique per place
curl -X POST https://scanly.app/api/v1/customers \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Olga Petrova",
  "phone": "+4915112345678",
  "email": "olga@example.com",
  "birthday": "1990-05-17",
  "locale": "en",
  "marketing_opt_in": false,
  "note": "Allergic to latex gloves",
  "tags": [
    "VIP"
  ],
  "external_id": "pos-42"
}'
Resposta · 201
{
  "object": "customer",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "name": "Olga Petrova",
  "phone": "+4915112345678",
  "email": "olga@example.com",
  "birthday": "1990-05-17",
  "locale": "en",
  "marketing_opt_in": false,
  "note": "Allergic to latex gloves",
  "tags": [
    "VIP"
  ],
  "external_id": "pos-42",
  "created_at": "2026-10-29T12:00:00.000Z",
  "updated_at": "2026-10-29T12:00:00.000Z"
}
GET/customers/{id}

Get a guest

Escopo: customers:read

Parâmetros

  • id*path · stringId
curl https://scanly.app/api/v1/customers/4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "customer",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "name": "Olga Petrova",
  "phone": "+4915112345678",
  "email": "olga@example.com",
  "birthday": "1990-05-17",
  "locale": "en",
  "marketing_opt_in": false,
  "note": "Allergic to latex gloves",
  "tags": [
    "VIP"
  ],
  "external_id": "pos-42",
  "created_at": "2026-10-29T12:00:00.000Z",
  "updated_at": "2026-10-29T12:00:00.000Z",
  "stats": {
    "visits": 1,
    "upcoming": 1,
    "no_shows": 1,
    "spent": 1,
    "last_visit_at": "2026-11-03T09:00:00.000Z"
  }
}
PATCH/customers/{id}

Update a guest

Only the fields you send change; send null to clear one.

Escopo: customers:write · Erros: customer_exists

Parâmetros

  • id*path · stringId

Corpo

  • namestringFull name
  • phonestring · nullableAny format; stored as +<country><number> when it has a country code
  • emailstring · email · nullable
  • birthdaystring · nullableYYYY-MM-DD, or MM-DD when the year is unknown
  • localestring · nullableLanguage for messages, e.g. en, de, ru
  • marketing_opt_inbooleanThe guest agreed to news and offers
  • notestring · nullable
  • tagsstring[]
  • external_idstring · nullableYour system's id; unique per place
curl -X PATCH https://scanly.app/api/v1/customers/4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Olga Petrova",
  "phone": "+4915112345678",
  "email": "olga@example.com",
  "birthday": "1990-05-17",
  "locale": "en",
  "marketing_opt_in": false,
  "note": "Allergic to latex gloves",
  "tags": [
    "VIP"
  ],
  "external_id": "pos-42"
}'
Resposta · 200
{
  "object": "customer",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "name": "Olga Petrova",
  "phone": "+4915112345678",
  "email": "olga@example.com",
  "birthday": "1990-05-17",
  "locale": "en",
  "marketing_opt_in": false,
  "note": "Allergic to latex gloves",
  "tags": [
    "VIP"
  ],
  "external_id": "pos-42",
  "created_at": "2026-10-29T12:00:00.000Z",
  "updated_at": "2026-10-29T12:00:00.000Z"
}
DELETE/customers/{id}

Delete a guest

Erases the guest (GDPR right to erasure): their contact details also disappear from past appointments; appointments and loyalty cards stay without a name.

Escopo: customers:write

Parâmetros

  • id*path · stringId
curl -X DELETE https://scanly.app/api/v1/customers/4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
Resposta · 200
{
  "object": "…",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "deleted": true
}

Referência

Loyalty

Cards, stamps, points, discounts and rewards — what a POS needs at the till.

GET/loyalty/cards

List loyalty cards

Escopo: loyalty:read

Parâmetros

  • limitquery · integerHow many to return, 1–100 (default 20).
  • cursorquery · stringnext_cursor of the previous page.
  • customer_idquery · string · uuidOnly this guest's cards
curl https://scanly.app/api/v1/loyalty/cards \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "list",
  "data": [
    {
      "object": "loyalty_card",
      "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
      "code": "K7F3-QX2M",
      "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
      "program_type": "stamps",
      "stamps": 3,
      "rewards": 0,
      "total_stamps": 13,
      "points": 1200,
      "total_spent": 12000,
      "currency": "EUR",
      "last_visit_at": "2026-11-03T09:00:00.000Z",
      "created_at": "2026-10-29T12:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
POST/loyalty/cards

Issue a loyalty card

A new card for the place's program, optionally for a guest. The guest opens it at /c/{code} to add it to Apple or Google Wallet.

Escopo: loyalty:write · Erros: loyalty_not_set_up

Corpo

  • customer_idstring · uuid · nullableThe guest it belongs to
curl -X POST https://scanly.app/api/v1/loyalty/cards \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d"
}'
Resposta · 201
{
  "object": "loyalty_card",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "code": "K7F3-QX2M",
  "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
  "program_type": "stamps",
  "stamps": 3,
  "rewards": 0,
  "total_stamps": 13,
  "points": 1200,
  "total_spent": 12000,
  "currency": "EUR",
  "last_visit_at": "2026-11-03T09:00:00.000Z",
  "created_at": "2026-10-29T12:00:00.000Z"
}
GET/loyalty/cards/{card}

Get a card

{card} is the card id or the code the guest shows (from the QR code or typed: K7F3-QX2M, k7f3qx2m).

Escopo: loyalty:read

Parâmetros

  • card*path · stringCard id, or the code on the card (K7F3-QX2M)
curl https://scanly.app/api/v1/loyalty/cards/K7F3-QX2M \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "loyalty_card",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "code": "K7F3-QX2M",
  "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
  "program_type": "stamps",
  "stamps": 3,
  "rewards": 0,
  "total_stamps": 13,
  "points": 1200,
  "total_spent": 12000,
  "currency": "EUR",
  "last_visit_at": "2026-11-03T09:00:00.000Z",
  "created_at": "2026-10-29T12:00:00.000Z"
}
PATCH/loyalty/cards/{card}

Link a card to a guest

Escopo: loyalty:write

Parâmetros

  • card*path · stringCard id, or the code on the card (K7F3-QX2M)

Corpo

  • customer_id*string · uuid · nullableThe guest, or null to unlink
curl -X PATCH https://scanly.app/api/v1/loyalty/cards/K7F3-QX2M \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d"
}'
Resposta · 200
{
  "object": "loyalty_card",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "code": "K7F3-QX2M",
  "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
  "program_type": "stamps",
  "stamps": 3,
  "rewards": 0,
  "total_stamps": 13,
  "points": 1200,
  "total_spent": 12000,
  "currency": "EUR",
  "last_visit_at": "2026-11-03T09:00:00.000Z",
  "created_at": "2026-10-29T12:00:00.000Z"
}
POST/loyalty/cards/{card}/quote

Preview a bill

What the card means for a bill before the guest pays: the discount of a discount card, the points a points card can spend and would earn, rewards ready on a stamp card. Changes nothing.

Escopo: loyalty:read

Parâmetros

  • card*path · stringCard id, or the code on the card (K7F3-QX2M)

Corpo

  • amount*integerThe bill before loyalty, in minor units
curl -X POST https://scanly.app/api/v1/loyalty/cards/K7F3-QX2M/quote \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "amount": 12000
}'
Resposta · 200
{
  "object": "loyalty_quote",
  "card_id": "a7b6c5d4-e3f2-4a1b-8c9d-0e1f2a3b4c5d",
  "program_type": "stamps",
  "amount": 12000,
  "currency": "EUR",
  "discount_percent": 1,
  "discount_amount": 1,
  "points_available": 1,
  "points_max_redeemable": 1,
  "points_to_earn": 1,
  "rewards_available": 1
}
POST/loyalty/cards/{card}/stamps

Add stamps

Stamp cards: every full card becomes a reward and the count starts over. The guest's Wallet pass and app update at once.

Escopo: loyalty:write

Parâmetros

  • card*path · stringCard id, or the code on the card (K7F3-QX2M)

Corpo

  • countintegerStamps to add, 1–20
  • referencestringYour receipt or transaction id, stored with it and sent in webhooks
curl -X POST https://scanly.app/api/v1/loyalty/cards/K7F3-QX2M/stamps \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "count": 1,
  "reference": "R-1001"
}'
Resposta · 201
{
  "transaction": {
    "object": "loyalty_transaction",
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "card_id": "a7b6c5d4-e3f2-4a1b-8c9d-0e1f2a3b4c5d",
    "type": "stamp",
    "amount": 12000,
    "points_earned": 1200,
    "currency": "EUR",
    "reference": "R-1001",
    "reverses": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "reversed_at": "2026-11-03T09:00:00.000Z",
    "created_at": "2026-10-29T12:00:00.000Z"
  },
  "card": {
    "object": "loyalty_card",
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "code": "K7F3-QX2M",
    "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
    "program_type": "stamps",
    "stamps": 3,
    "rewards": 0,
    "total_stamps": 13,
    "points": 1200,
    "total_spent": 12000,
    "currency": "EUR",
    "last_visit_at": "2026-11-03T09:00:00.000Z",
    "created_at": "2026-10-29T12:00:00.000Z"
  }
}
POST/loyalty/cards/{card}/purchases

Record a purchase

Records a paid bill on the card. Points cards earn the program's cashback in points; discount cards add to the guest's spend, which moves them up discount levels. Send the amount actually paid, after any discount or points used.

Escopo: loyalty:write

Parâmetros

  • card*path · stringCard id, or the code on the card (K7F3-QX2M)

Corpo

  • amount*integerThe bill, in minor units
  • referencestringYour receipt or transaction id, stored with it and sent in webhooks
curl -X POST https://scanly.app/api/v1/loyalty/cards/K7F3-QX2M/purchases \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "amount": 12000,
  "reference": "R-1001"
}'
Resposta · 201
{
  "transaction": {
    "object": "loyalty_transaction",
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "card_id": "a7b6c5d4-e3f2-4a1b-8c9d-0e1f2a3b4c5d",
    "type": "stamp",
    "amount": 12000,
    "points_earned": 1200,
    "currency": "EUR",
    "reference": "R-1001",
    "reverses": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "reversed_at": "2026-11-03T09:00:00.000Z",
    "created_at": "2026-10-29T12:00:00.000Z"
  },
  "card": {
    "object": "loyalty_card",
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "code": "K7F3-QX2M",
    "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
    "program_type": "stamps",
    "stamps": 3,
    "rewards": 0,
    "total_stamps": 13,
    "points": 1200,
    "total_spent": 12000,
    "currency": "EUR",
    "last_visit_at": "2026-11-03T09:00:00.000Z",
    "created_at": "2026-10-29T12:00:00.000Z"
  }
}
POST/loyalty/cards/{card}/redemptions

Spend points or redeem a reward

type: "points" takes amount points (minor units) off the balance, e.g. as a discount on the bill; type: "reward" redeems one earned reward of a stamp card. Fails with 409 when the balance is too low; nothing changes then.

Escopo: loyalty:write · Erros: insufficient_pointsno_reward

Parâmetros

  • card*path · stringCard id, or the code on the card (K7F3-QX2M)

Corpo (1/2)

  • type*string
  • amount*integerPoints to spend
  • referencestringYour receipt or transaction id, stored with it and sent in webhooks

Corpo (2/2)

  • type*string
  • referencestringYour receipt or transaction id, stored with it and sent in webhooks
curl -X POST https://scanly.app/api/v1/loyalty/cards/K7F3-QX2M/redemptions \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "type": "points",
  "amount": 12000,
  "reference": "R-1001"
}'
Resposta · 201
{
  "transaction": {
    "object": "loyalty_transaction",
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "card_id": "a7b6c5d4-e3f2-4a1b-8c9d-0e1f2a3b4c5d",
    "type": "stamp",
    "amount": 12000,
    "points_earned": 1200,
    "currency": "EUR",
    "reference": "R-1001",
    "reverses": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "reversed_at": "2026-11-03T09:00:00.000Z",
    "created_at": "2026-10-29T12:00:00.000Z"
  },
  "card": {
    "object": "loyalty_card",
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "code": "K7F3-QX2M",
    "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
    "program_type": "stamps",
    "stamps": 3,
    "rewards": 0,
    "total_stamps": 13,
    "points": 1200,
    "total_spent": 12000,
    "currency": "EUR",
    "last_visit_at": "2026-11-03T09:00:00.000Z",
    "created_at": "2026-10-29T12:00:00.000Z"
  }
}
GET/loyalty/cards/{card}/transactions

List a card's transactions

Escopo: loyalty:read

Parâmetros

  • card*path · stringCard id, or the code on the card (K7F3-QX2M)
  • limitquery · integerHow many to return, 1–100 (default 20).
  • cursorquery · stringnext_cursor of the previous page.
curl https://scanly.app/api/v1/loyalty/cards/K7F3-QX2M/transactions \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "list",
  "data": [
    {
      "object": "loyalty_transaction",
      "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
      "card_id": "a7b6c5d4-e3f2-4a1b-8c9d-0e1f2a3b4c5d",
      "type": "stamp",
      "amount": 12000,
      "points_earned": 1200,
      "currency": "EUR",
      "reference": "R-1001",
      "reverses": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
      "reversed_at": "2026-11-03T09:00:00.000Z",
      "created_at": "2026-10-29T12:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
POST/loyalty/transactions/{id}/reverse

Reverse a transaction

Undoes a transaction once: a refunded purchase takes back its spend and the points it earned, spent points and redeemed rewards come back, stamps are removed. Returns the new reversal transaction.

Escopo: loyalty:write · Erros: already_reversedinsufficient_pointsinsufficient_stamps

Parâmetros

  • id*path · stringId

Corpo

  • referencestringYour receipt or transaction id, stored with it and sent in webhooks
curl -X POST https://scanly.app/api/v1/loyalty/transactions/4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c/reverse \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "reference": "R-1001"
}'
Resposta · 201
{
  "transaction": {
    "object": "loyalty_transaction",
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "card_id": "a7b6c5d4-e3f2-4a1b-8c9d-0e1f2a3b4c5d",
    "type": "stamp",
    "amount": 12000,
    "points_earned": 1200,
    "currency": "EUR",
    "reference": "R-1001",
    "reverses": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "reversed_at": "2026-11-03T09:00:00.000Z",
    "created_at": "2026-10-29T12:00:00.000Z"
  },
  "card": {
    "object": "loyalty_card",
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "code": "K7F3-QX2M",
    "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
    "program_type": "stamps",
    "stamps": 3,
    "rewards": 0,
    "total_stamps": 13,
    "points": 1200,
    "total_spent": 12000,
    "currency": "EUR",
    "last_visit_at": "2026-11-03T09:00:00.000Z",
    "created_at": "2026-10-29T12:00:00.000Z"
  }
}

Referência

Bookings

Appointments and reservations: specialists, free times, booking, moving and cancelling.

GET/specialists

List specialists

Who does the work, in the place's order, with the services they do.

Escopo: bookings:read

curl https://scanly.app/api/v1/specialists \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "list",
  "data": [
    {
      "object": "specialist",
      "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
      "name": "Olga Petrova",
      "title": {},
      "photo_url": null,
      "active": true,
      "service_ids": [
        "8bc29f32-55fc-4714-afac-559714a24454"
      ]
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/availability

Free start times

Free start times for one or more services done one after the other, day by day. Follows the place's booking rules (notice, how far ahead, breaks) unless ignore_rules=true, which shows every free time within working hours, like staff see it.

Escopo: bookings:read

Parâmetros

  • service_ids*query · stringComma-separated service ids
  • specialist_idquery · string · uuidOnly this specialist; default: anyone who does the services
  • datequery · stringFirst local date, default today
  • daysquery · integerHow many days, 1–31 (default 7)
  • ignore_rulesquery · "true" | "false"true: every free time within working hours, without the online notice and horizon
curl https://scanly.app/api/v1/availability \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "availability",
  "timezone": "Europe/Berlin",
  "days": [
    {
      "date": "2026-11-03",
      "times": [
        {
          "start": "2026-11-03T09:00:00.000Z",
          "specialist_ids": [
            "e7794c54-65aa-4880-b12a-ba651444f40b"
          ]
        }
      ]
    }
  ]
}
GET/bookings

List bookings

Appointments starting in [from, to), earliest first. Default: from now, the next 30 days.

Escopo: bookings:read

Parâmetros

  • fromquery · string · date-time
  • toquery · string · date-time
  • statusquery · "pending" | "confirmed" | "completed" | "no_show" | "cancelled"
  • customer_idquery · string · uuid
  • specialist_idquery · string · uuid
  • limitquery · integer
  • cursorquery · string
curl https://scanly.app/api/v1/bookings \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "list",
  "data": [
    {
      "object": "booking",
      "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
      "code": "K7F3-QX2M",
      "status": "confirmed",
      "cancelled_by": "guest",
      "starts_at": "2026-11-03T09:00:00.000Z",
      "ends_at": "2026-11-03T10:00:00.000Z",
      "kind": "service",
      "specialist": {
        "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
        "name": "Olga Petrova"
      },
      "table": {
        "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
        "name": "Olga Petrova"
      },
      "party_size": 4,
      "guest_confirmed": false,
      "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
      "guest": {
        "name": "Olga Petrova",
        "phone": "+4915112345678",
        "email": "olga@example.com",
        "telegram": "@olga_p",
        "locale": "en"
      },
      "note": "Allergic to latex gloves",
      "services": [
        {
          "service_id": "8bc29f32-55fc-4714-afac-559714a24454",
          "name": "Olga Petrova",
          "price": 4500,
          "price_from": false,
          "duration_minutes": 60
        }
      ],
      "total": 4500,
      "currency": "EUR",
      "source": "api",
      "created_at": "2026-10-29T12:00:00.000Z",
      "updated_at": "2026-10-29T12:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
POST/bookings

Book an appointment

Books one or more services back to back with a specialist (or anyone free with specialist_id: null, who is picked to spread the work evenly). Give an existing customer_id, or guest details: a guest with the same phone or email is reused, else added to the guest list. The guest gets the usual confirmation by email, Telegram or the app. Without ignore_rules the time must be a free start time from /availability.

Escopo: bookings:write · Erros: slot_takenoutside_hoursservicesunavailablecontact_required

Corpo

  • service_ids*string · uuid[]
  • specialist_idstring · uuid · nullable
  • starts_at*string · date-time
  • customer_idstring · uuid
  • guestobject
  • notestring
  • ignore_rulesbooleanBook outside the online rules and working hours, like staff can (never over another appointment)
curl -X POST https://scanly.app/api/v1/bookings \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "service_ids": [
    "8bc29f32-55fc-4714-afac-559714a24454"
  ],
  "specialist_id": "e7794c54-65aa-4880-b12a-ba651444f40b",
  "starts_at": "2026-11-03T09:00:00.000Z",
  "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
  "guest": {
    "name": "Olga Petrova",
    "phone": "+4915112345678",
    "email": "olga@example.com",
    "telegram": "@olga_p",
    "locale": "en"
  },
  "note": "Allergic to latex gloves",
  "ignore_rules": false
}'
Resposta · 201
{
  "object": "booking",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "code": "K7F3-QX2M",
  "status": "confirmed",
  "cancelled_by": "guest",
  "starts_at": "2026-11-03T09:00:00.000Z",
  "ends_at": "2026-11-03T10:00:00.000Z",
  "kind": "service",
  "specialist": {
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "name": "Olga Petrova"
  },
  "table": {
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "name": "Olga Petrova"
  },
  "party_size": 4,
  "guest_confirmed": false,
  "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
  "guest": {
    "name": "Olga Petrova",
    "phone": "+4915112345678",
    "email": "olga@example.com",
    "telegram": "@olga_p",
    "locale": "en"
  },
  "note": "Allergic to latex gloves",
  "services": [
    {
      "service_id": "8bc29f32-55fc-4714-afac-559714a24454",
      "name": "Olga Petrova",
      "price": 4500,
      "price_from": false,
      "duration_minutes": 60
    }
  ],
  "total": 4500,
  "currency": "EUR",
  "source": "api",
  "created_at": "2026-10-29T12:00:00.000Z",
  "updated_at": "2026-10-29T12:00:00.000Z"
}
GET/bookings/{id}

Get a booking

Escopo: bookings:read

Parâmetros

  • id*path · stringId
curl https://scanly.app/api/v1/bookings/4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "booking",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "code": "K7F3-QX2M",
  "status": "confirmed",
  "cancelled_by": "guest",
  "starts_at": "2026-11-03T09:00:00.000Z",
  "ends_at": "2026-11-03T10:00:00.000Z",
  "kind": "service",
  "specialist": {
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "name": "Olga Petrova"
  },
  "table": {
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "name": "Olga Petrova"
  },
  "party_size": 4,
  "guest_confirmed": false,
  "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
  "guest": {
    "name": "Olga Petrova",
    "phone": "+4915112345678",
    "email": "olga@example.com",
    "telegram": "@olga_p",
    "locale": "en"
  },
  "note": "Allergic to latex gloves",
  "services": [
    {
      "service_id": "8bc29f32-55fc-4714-afac-559714a24454",
      "name": "Olga Petrova",
      "price": 4500,
      "price_from": false,
      "duration_minutes": 60
    }
  ],
  "total": 4500,
  "currency": "EUR",
  "source": "api",
  "created_at": "2026-10-29T12:00:00.000Z",
  "updated_at": "2026-10-29T12:00:00.000Z"
}
PATCH/bookings/{id}

Update a booking

Move it (starts_at, specialist_id: any time nobody else is booked, like staff), mark it confirmed, completed (the guest came: their loyalty card gets the visit) or no_show, or change the note. The guest hears about a new time and a confirmation.

Escopo: bookings:write · Erros: slot_takennot_upcoming

Parâmetros

  • id*path · stringId

Corpo

  • starts_atstring · date-time
  • specialist_idstring · uuidAppointments: another specialist
  • table_idstring · uuidReservations: another table
  • status"confirmed" | "completed" | "no_show"
  • notestring · nullable
curl -X PATCH https://scanly.app/api/v1/bookings/4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "starts_at": "2026-11-03T09:00:00.000Z",
  "specialist_id": "e7794c54-65aa-4880-b12a-ba651444f40b",
  "table_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
  "status": "confirmed",
  "note": "Allergic to latex gloves"
}'
Resposta · 200
{
  "object": "booking",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "code": "K7F3-QX2M",
  "status": "confirmed",
  "cancelled_by": "guest",
  "starts_at": "2026-11-03T09:00:00.000Z",
  "ends_at": "2026-11-03T10:00:00.000Z",
  "kind": "service",
  "specialist": {
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "name": "Olga Petrova"
  },
  "table": {
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "name": "Olga Petrova"
  },
  "party_size": 4,
  "guest_confirmed": false,
  "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
  "guest": {
    "name": "Olga Petrova",
    "phone": "+4915112345678",
    "email": "olga@example.com",
    "telegram": "@olga_p",
    "locale": "en"
  },
  "note": "Allergic to latex gloves",
  "services": [
    {
      "service_id": "8bc29f32-55fc-4714-afac-559714a24454",
      "name": "Olga Petrova",
      "price": 4500,
      "price_from": false,
      "duration_minutes": 60
    }
  ],
  "total": 4500,
  "currency": "EUR",
  "source": "api",
  "created_at": "2026-10-29T12:00:00.000Z",
  "updated_at": "2026-10-29T12:00:00.000Z"
}
POST/bookings/{id}/cancel

Cancel a booking

Cancels on behalf of the place (cancelled_by: business, the guest is told) or, with by: "guest", records that the guest cancelled.

Escopo: bookings:write · Erros: not_upcoming

Parâmetros

  • id*path · stringId

Corpo

  • by"business" | "guest"
curl -X POST https://scanly.app/api/v1/bookings/4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c/cancel \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "by": "business"
}'
Resposta · 200
{
  "object": "booking",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "code": "K7F3-QX2M",
  "status": "confirmed",
  "cancelled_by": "guest",
  "starts_at": "2026-11-03T09:00:00.000Z",
  "ends_at": "2026-11-03T10:00:00.000Z",
  "kind": "service",
  "specialist": {
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "name": "Olga Petrova"
  },
  "table": {
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "name": "Olga Petrova"
  },
  "party_size": 4,
  "guest_confirmed": false,
  "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
  "guest": {
    "name": "Olga Petrova",
    "phone": "+4915112345678",
    "email": "olga@example.com",
    "telegram": "@olga_p",
    "locale": "en"
  },
  "note": "Allergic to latex gloves",
  "services": [
    {
      "service_id": "8bc29f32-55fc-4714-afac-559714a24454",
      "name": "Olga Petrova",
      "price": 4500,
      "price_from": false,
      "duration_minutes": 60
    }
  ],
  "total": 4500,
  "currency": "EUR",
  "source": "api",
  "created_at": "2026-10-29T12:00:00.000Z",
  "updated_at": "2026-10-29T12:00:00.000Z"
}

Referência

Reservations

Restaurants: tables, free times for a party, reserving a table. Reservations are bookings with kind "table".

GET/tables

List tables

The tables of the floor plan, by room and name.

Escopo: bookings:read

curl https://scanly.app/api/v1/tables \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "list",
  "data": [
    {
      "object": "table",
      "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
      "name": "Olga Petrova",
      "zone": "Terrace",
      "seats": 4,
      "min_seats": 2,
      "online": false
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/tables/availability

Free reservation times

Start times when a table that fits the party is free, day by day. Follows the reservation rules (notice, how far ahead, online tables only) unless ignore_rules=true, which offers every table that seats the party, like staff see it.

Escopo: bookings:read

Parâmetros

  • party*query · integerHow many guests
  • table_idquery · string · uuidOnly this table
  • datequery · stringFirst local date, default today
  • daysquery · integerHow many days, 1–31 (default 7)
  • ignore_rulesquery · "true" | "false"true: every table that seats the party, without the online notice and horizon
curl https://scanly.app/api/v1/tables/availability \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "table_availability",
  "timezone": "Europe/Berlin",
  "duration_minutes": 60,
  "days": [
    {
      "date": "2026-11-03",
      "times": [
        {
          "start": "2026-11-03T09:00:00.000Z",
          "table_ids": [
            "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d"
          ]
        }
      ]
    }
  ]
}
POST/reservations

Reserve a table

Holds a table for a party for the place's reservation length. With table_id: null the smallest free table that fits is picked. Give an existing customer_id, or guest details with a phone, an email or a Telegram username. The result is a booking with kind: "table": read, move, confirm and cancel it under /bookings.

Escopo: bookings:write · Erros: slot_takenunavailableparty_too_largecontact_required

Corpo

  • party*integer
  • table_idstring · uuid · nullable
  • starts_at*string · date-time
  • customer_idstring · uuid
  • guestobject
  • notestring
  • ignore_rulesbooleanReserve outside the online rules and hours, like staff can (never over another reservation)
curl -X POST https://scanly.app/api/v1/reservations \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "party": 4,
  "table_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
  "starts_at": "2026-11-03T09:00:00.000Z",
  "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
  "guest": {
    "name": "Olga Petrova",
    "phone": "+4915112345678",
    "email": "olga@example.com",
    "telegram": "@olga_p",
    "locale": "en"
  },
  "note": "Allergic to latex gloves",
  "ignore_rules": false
}'
Resposta · 201
{
  "object": "booking",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "code": "K7F3-QX2M",
  "status": "confirmed",
  "cancelled_by": "guest",
  "starts_at": "2026-11-03T09:00:00.000Z",
  "ends_at": "2026-11-03T10:00:00.000Z",
  "kind": "service",
  "specialist": {
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "name": "Olga Petrova"
  },
  "table": {
    "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
    "name": "Olga Petrova"
  },
  "party_size": 4,
  "guest_confirmed": false,
  "customer_id": "c3d2a1b0-8f7e-4d6c-9b5a-1e2f3a4b5c6d",
  "guest": {
    "name": "Olga Petrova",
    "phone": "+4915112345678",
    "email": "olga@example.com",
    "telegram": "@olga_p",
    "locale": "en"
  },
  "note": "Allergic to latex gloves",
  "services": [
    {
      "service_id": "8bc29f32-55fc-4714-afac-559714a24454",
      "name": "Olga Petrova",
      "price": 4500,
      "price_from": false,
      "duration_minutes": 60
    }
  ],
  "total": 4500,
  "currency": "EUR",
  "source": "api",
  "created_at": "2026-10-29T12:00:00.000Z",
  "updated_at": "2026-10-29T12:00:00.000Z"
}

Referência

Orders

Table orders and orders ahead for pickup.

GET/orders

List orders

Table orders and orders ahead for pickup, newest first.

Escopo: orders:read

Parâmetros

  • limitquery · integerHow many to return, 1–100 (default 20).
  • cursorquery · stringnext_cursor of the previous page.
  • statusquery · "new" | "accepted" | "ready" | "served" | "cancelled"
  • sincequery · string · date-timeOnly orders placed at or after this time
curl https://scanly.app/api/v1/orders \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "list",
  "data": [
    {
      "object": "order",
      "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
      "number": 17,
      "status": "new",
      "kind": "table",
      "table": 4,
      "pickup_name": "Alex",
      "pickup_phone": "+49 151 2345678",
      "pickup_at": "2026-11-03T09:00:00.000Z",
      "note": "Allergic to latex gloves",
      "items": [
        {
          "item_id": "0d1c2b3a-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
          "name": "Olga Petrova",
          "price": 4500,
          "quantity": 2
        }
      ],
      "total": 4500,
      "currency": "EUR",
      "locale": "en",
      "created_at": "2026-10-29T12:00:00.000Z",
      "updated_at": "2026-10-29T12:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET/orders/{id}

Get an order

Escopo: orders:read

Parâmetros

  • id*path · stringId
curl https://scanly.app/api/v1/orders/4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c \
  -H "Authorization: Bearer $SCANLY_KEY"
Resposta · 200
{
  "object": "order",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "number": 17,
  "status": "new",
  "kind": "table",
  "table": 4,
  "pickup_name": "Alex",
  "pickup_phone": "+49 151 2345678",
  "pickup_at": "2026-11-03T09:00:00.000Z",
  "note": "Allergic to latex gloves",
  "items": [
    {
      "item_id": "0d1c2b3a-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
      "name": "Olga Petrova",
      "price": 4500,
      "quantity": 2
    }
  ],
  "total": 4500,
  "currency": "EUR",
  "locale": "en",
  "created_at": "2026-10-29T12:00:00.000Z",
  "updated_at": "2026-10-29T12:00:00.000Z"
}
PATCH/orders/{id}

Change an order's status

new → accepted → ready → served, or cancelled. The guest sees it live (and on the lock screen in the Scanly app).

Escopo: orders:write

Parâmetros

  • id*path · stringId

Corpo

  • status*"new" | "accepted" | "ready" | "served" | "cancelled"
curl -X PATCH https://scanly.app/api/v1/orders/4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c \
  -H "Authorization: Bearer $SCANLY_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "new"
}'
Resposta · 200
{
  "object": "order",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "number": 17,
  "status": "new",
  "kind": "table",
  "table": 4,
  "pickup_name": "Alex",
  "pickup_phone": "+49 151 2345678",
  "pickup_at": "2026-11-03T09:00:00.000Z",
  "note": "Allergic to latex gloves",
  "items": [
    {
      "item_id": "0d1c2b3a-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
      "name": "Olga Petrova",
      "price": 4500,
      "quantity": 2
    }
  ],
  "total": 4500,
  "currency": "EUR",
  "locale": "en",
  "created_at": "2026-10-29T12:00:00.000Z",
  "updated_at": "2026-10-29T12:00:00.000Z"
}