Разработчикам

Scanly API

Подключите кассу, киоск, CRM или собственный сайт к гостям, картам лояльности, записям и заказам заведения. REST с JSON, вебхуки, документ OpenAPI.

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

Справочник — на английском, как и сам 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.

Руководства

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".

Руководства

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"}'

Руководства

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.

Руководства

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));
  });
}

Ошибки

  • 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.

Справочник

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.

Доступ: restaurant:read

curl https://scanly.app/api/v1/restaurant \
  -H "Authorization: Bearer $SCANLY_KEY"
Ответ · 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
      }
    ]
  }
}

Справочник

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.

Доступ: catalog:read

curl https://scanly.app/api/v1/catalog \
  -H "Authorization: Bearer $SCANLY_KEY"
Ответ · 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.

Доступ: catalog:write

Параметры

  • id*path · stringId

Тело

  • 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
}'
Ответ · 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"
  ]
}

Справочник

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.

Доступ: customers:read

Параметры

  • 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"
Ответ · 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.

Доступ: customers:write · Ошибки: customer_exists

Тело

  • 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"
}'
Ответ · 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

Доступ: customers:read

Параметры

  • id*path · stringId
curl https://scanly.app/api/v1/customers/4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c \
  -H "Authorization: Bearer $SCANLY_KEY"
Ответ · 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.

Доступ: customers:write · Ошибки: customer_exists

Параметры

  • id*path · stringId

Тело

  • 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"
}'
Ответ · 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.

Доступ: customers:write

Параметры

  • 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)"
Ответ · 200
{
  "object": "…",
  "id": "4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c",
  "deleted": true
}

Справочник

Loyalty

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

GET/loyalty/cards

List loyalty cards

Доступ: loyalty:read

Параметры

  • 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"
Ответ · 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.

Доступ: loyalty:write · Ошибки: loyalty_not_set_up

Тело

  • 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"
}'
Ответ · 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).

Доступ: loyalty:read

Параметры

  • 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"
Ответ · 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

Доступ: loyalty:write

Параметры

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

Тело

  • 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"
}'
Ответ · 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.

Доступ: loyalty:read

Параметры

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

Тело

  • 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
}'
Ответ · 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.

Доступ: loyalty:write

Параметры

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

Тело

  • 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"
}'
Ответ · 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.

Доступ: loyalty:write

Параметры

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

Тело

  • 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"
}'
Ответ · 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.

Доступ: loyalty:write · Ошибки: insufficient_pointsno_reward

Параметры

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

Тело (1/2)

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

Тело (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"
}'
Ответ · 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

Доступ: loyalty:read

Параметры

  • 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"
Ответ · 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.

Доступ: loyalty:write · Ошибки: already_reversedinsufficient_pointsinsufficient_stamps

Параметры

  • id*path · stringId

Тело

  • 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"
}'
Ответ · 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"
  }
}

Справочник

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.

Доступ: bookings:read

curl https://scanly.app/api/v1/specialists \
  -H "Authorization: Bearer $SCANLY_KEY"
Ответ · 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.

Доступ: bookings:read

Параметры

  • 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"
Ответ · 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.

Доступ: bookings:read

Параметры

  • 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"
Ответ · 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.

Доступ: bookings:write · Ошибки: slot_takenoutside_hoursservicesunavailablecontact_required

Тело

  • 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
}'
Ответ · 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

Доступ: bookings:read

Параметры

  • id*path · stringId
curl https://scanly.app/api/v1/bookings/4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c \
  -H "Authorization: Bearer $SCANLY_KEY"
Ответ · 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.

Доступ: bookings:write · Ошибки: slot_takennot_upcoming

Параметры

  • id*path · stringId

Тело

  • 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"
}'
Ответ · 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.

Доступ: bookings:write · Ошибки: not_upcoming

Параметры

  • id*path · stringId

Тело

  • 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"
}'
Ответ · 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"
}

Справочник

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.

Доступ: bookings:read

curl https://scanly.app/api/v1/tables \
  -H "Authorization: Bearer $SCANLY_KEY"
Ответ · 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.

Доступ: bookings:read

Параметры

  • 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"
Ответ · 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.

Доступ: bookings:write · Ошибки: slot_takenunavailableparty_too_largecontact_required

Тело

  • 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
}'
Ответ · 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"
}

Справочник

Orders

Table orders and orders ahead for pickup.

GET/orders

List orders

Table orders and orders ahead for pickup, newest first.

Доступ: orders:read

Параметры

  • 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"
Ответ · 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

Доступ: orders:read

Параметры

  • id*path · stringId
curl https://scanly.app/api/v1/orders/4f8a2c1e-9b7d-4e3a-8c6f-2d1b0a9e8f7c \
  -H "Authorization: Bearer $SCANLY_KEY"
Ответ · 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).

Доступ: orders:write

Параметры

  • id*path · stringId

Тело

  • 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"
}'
Ответ · 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"
}