Create an order
Create a booking, the same rules as the studio's public booking page.
Also callable with a publishable key (shyni_pk_...).
Books the requested window exactly as a customer would on the studio's own booking
page: same pricing, same availability and capacity checks, same deposit rules, same
gift-voucher handling. Take scheduledStart / scheduledEnd from one entry of
GET /api/v1/availability/starts, a pair that does not match the services' real
duration is refused with 400 INVALID_INPUT.
By default the customer receives the same e-mails as after a public booking (confirmation,
payment received, reminder, cancellation, ...); the studio owner can switch ALL of them off
per API key (then you mail the customer yourself). The switch is read when the order is
created and stays with that order - toggling the key later does not change existing orders.
The response always carries orderUrl, the customer's Shyni page for this order
(cancel, reschedule). It contains the order's access token: hand it to the customer only.
Booking with a deposit. If the services need a deposit, the order is created awaiting
it (state: CREATED, depositState: PENDING, depositRequired: true, depositAmount)
and the slot is held for a limited time, as on the booking page. When the studio takes
online payments, the response also carries paymentUrl, the studio's payment-gateway
page for exactly that deposit. Redirect the customer there; after paying, the gateway
returns them to their Shyni order page (orderUrl), which shows the booking as paid
and confirmed. Without paymentUrl (the studio takes the deposit by bank transfer, or
the gateway is briefly unavailable) the customer pays through orderUrl. Unpaid
deposits release the slot when the hold runs out; holdUntil in the response says
when: 30 minutes after creation when the studio takes card payments through the payment
gateway, 3 hours when the deposit is paid by bank transfer. Example:
curl -X POST https://api.shyni.dev/api/v1/orders \ -H "Authorization: Bearer shyni_sk_live_..." \ -H "Idempotency-Key: 6f1c2a1e-7d3b-4c9a-8e2f-1b5a9c0d4e6f" \ -H "Content-Type: application/json" \ -d '{"serviceIds":["3b7e9c52-81d4-4f6a-a0c3-2d9e5b8f1a47"], "scheduledStart":"2026-11-03T08:00:00Z","scheduledEnd":"2026-11-03T09:30:00Z", "customer":{"name":"Jan Novak","email":"jan@example.cz","phone":"+420 777 123 456"}, "variant":"MEDIUM"}'# 201 -> { "depositRequired": true, "depositAmount": 50000,# "paymentUrl": "https://gate.gopay.cz/gw/v3/...", "orderUrl": "https://app.shyni.cz/order/...", ... }Failures to branch on: 409 SLOT_UNAVAILABLE (the window is taken, pick another
start); 409 TERMS_VERSION_STALE (the studio's terms changed since you read GET /studio, show the current ones and resend with acceptedTermsVersion = terms.version); 409 STUDIO_NOT_PUBLISHED (the studio is not published; a test studio is always bookable); 400 INVALID_INPUT with the studio's own sentence when the studio has paused
online bookings, or for a service the studio prices by hand (those are inquiries, not
bookings). Requires the orders:write scope and an Idempotency-Key header (see
Idempotency above); a replay returns the first answer and creates no second order.
ApiKeyAuth orders:writeAuthorizationBearer <shyni_sk_live_*>An API key, issued once and never retrievable again: an organization key in the developer
portal, or a studio key in the studio's admin (Moje studio → API). Send as Authorization: Bearer shyni_sk_live_....
Scope: orders:write
Idempotency-Key*stringCaller-chosen idempotency token for this write. A retry under the SAME key with
the SAME body replays the first response; the SAME key with a DIFFERENT body is
422 IDEMPOTENCY_KEY_MISMATCH. See Idempotency above.
length <= 64Shyni-Customer-IP?stringPOST /orders, POST /quotes, POST /quotes/batch and POST /gift-vouchers/purchases with a secret key only: the end customer's IPv4 or IPv6 address as your server
saw it. When present, the storefront's per-IP and per-studio booking (quotes: price preview; gift voucher purchases: purchase) budgets apply to that address
(shared with the studio's booking page); an invalid value is 400 INVALID_INPUT. Ignored with a
publishable key (the browser's address is used) and on every other endpoint.
application/json- body
Every field has a default, so a missing field is refused as 400 INVALID_INPUT with
a message naming it (not a shapeless framework error), serviceIds,
scheduledStart, scheduledEnd, customer.name, customer.email,
customer.phone and variant are required (a missing one is 400 INVALID_INPUT).
serviceIds*array<string>Service and/or package ids from GET /api/v1/services.
scheduledStart*|Start of the window, the start of an entry from GET /api/v1/availability/starts.
date-timescheduledEnd*|End of the window, the end of the SAME entry.
date-timecustomer*variant*|Code of the variant the order is priced as and scheduled with (for detailing, the vehicle size:
SMALL, MEDIUM, LARGE, XL, SUV, XXL, MOTO). Take it from the variants of the
services in GET /api/v1/services. Required; an unknown code is 400 INVALID_INPUT with field
variant (for a GENERIC studio the codes of GET /catalog/variants). The variant's extraMinutes lengthen the work: pass the same variant to
GET /api/v1/availability/starts so the scheduledEnd you send matches.
subject?What the customer brings, checked against the studio's subject type (GET /subject-types). Optional for a studio
that defines no type. A refusal is 400 INVALID_SUBJECT with field naming the attribute key (or subject.type).
note?string|nullexternalRef?|Your own id of the order (e.g. your system's booking number), at most 64 characters, unique per studio: reusing one is 409 CONFLICT with field externalRef. Another studio may use the same value. Read back on the order and searchable with GET /orders?externalRef=.
voucherCode?|A gift voucher or discount code, applied exactly as on the booking page.
optionAnswers?array<>Configurator answers, for services that ask questions: questionId and optionId come from questions of the service in GET /api/v1/services.
sourceLabel?|Optional name of your integration, stored on the order as its source label. Sanitized to [a-z0-9-], max 40 characters (Partner Web! becomes partner-web). The order's channel is always API; it cannot be set here.
attribution?Where the customer came from, for conversion measurement on your side. Every field is an optional string;
a value over its cap is 400 INVALID_INPUT with field attribution.<name> (nothing is cut silently).
An unknown key is 400 UNKNOWN_FIELD with field attribution.<key>. The order list and webhooks return
the same values in attribution.
metadata?|Your own flat key-value data, kept on the order and returned unchanged by GET /api/v1/orders/{id},
the order list and the order.* webhooks. Free-form: any key that follows the rules below is accepted
(this is the one request object where unknown keys are expected). Caps: at most 20 keys; a key is
1-40 characters of A-Z a-z 0-9 _ . -; a value is a string of at most 500 characters (a number,
boolean, null, array or object value is 400 INVALID_INPUT with field metadata.<key>; too many
keys is 400 with field metadata). It may be seen by the studio: do not put secrets in it.
billing?|Optional company to invoice. company is required whenever any other key is sent
(400 INVALID_INPUT, field billing.company); company is at most 160 characters, ico and dic
at most 16, address at most 255 (400 with field billing.<key>). An unknown key is
400 UNKNOWN_FIELD. Stored on the order and returned as billing by the order endpoints and order.*
webhooks; deleted when the customer is erased. Shyni does not issue invoices from it.
inspection?booleantrue books the studio's free inspection instead of the order: kind INSPECTION, totalPrice 0, no deposit
(so no paymentUrl), the window as long as the studio's inspection length (take it from
GET /availability/starts?inspection=true). serviceIds must hold a service whose group offers an inspection,
else 400 INVALID_INPUT (field: serviceIds); a voucherCode is refused. A contact (same e-mail or same phone)
can have one upcoming inspection: a second booking is 409 INSPECTION_ALREADY_BOOKED. Quoted (from-price or
on-request) services can be inspected. Add the customer's photos with POST /orders/{id}/inspection-photos.
falseacceptedTermsVersion?integerThe studio terms version you showed the customer (terms.version from GET /studio). Send it when your
site shows the studio's terms; omit it when you do not (nothing is recorded then). A version other than the
studio's current one is 409 TERMS_VERSION_STALE (field: acceptedTermsVersion). A studio without terms
accepts any value and records nothing.
knownCarToken?stringProof of a known car for services with orderAudience KNOWN_CARS: the token of a return-reminder link, or the token a storefront customer gets by confirming a code sent to their e-mail (the studio's storefront only, not an API endpoint).
It must belong to the car in subject, and that car must have a completed order at the studio. Anything else
(absent, invalid, expired, another studio, a car without a completed order) is the same 422 SERVICE_KNOWN_CARS_ONLY.
waiveDayAdjustment?booleanSecret key only. Waives the day-policy surcharge (a VIP day) of this one order; the waived amount comes back as waivedDayAdjustment. A day discount is never waived. A publishable key sending true is 403 FORBIDDEN with field waiveDayAdjustment and nothing is created; false or absent is fine for both key kinds.
falseskipDeposit?booleanSecret key only. No deposit for this order: depositState NOT_REQUIRED, depositAmount 0, no hold and no paymentUrl. A publishable key sending true is 403 FORBIDDEN with field skipDeposit and nothing is created; false or absent is fine for both key kinds.
falseThe created order.
application/json- response
Money fields are minor units of currency. orderUrl carries the order's access
token, a secret for the customer, not for logs.
id*stringpublicId*stringstate*OrderState"CREATED""CONFIRMED""IN_PROGRESS""DONE""CANCELLED"kind?OrderKindORDER: an ordinary booking. INSPECTION: the studio's free inspection (price 0, no deposit), booked with inspection: true in POST /orders; finish it with POST /orders/{id}/inspection/complete.
"ORDER""INSPECTION"depositState*DepositStatePENDING: a deposit above zero is owed and unpaid on a live order. PAID: the deposit was received. REFUNDED: it was given back. NOT_REQUIRED: no deposit is owed, either because the order never had one (deposit 0, free inspection) or because the order was cancelled before the deposit was paid. A cancelled order whose deposit was already PAID or REFUNDED keeps that state, so depositState=PENDING lists only deposits that still have to be collected.
"PENDING""PAID""REFUNDED""NOT_REQUIRED"currency*stringtotalPrice*integerint64depositRequired*booleanTrue when the order awaits a deposit (depositAmount > 0).
depositAmount*integerint64scheduledStart*stringdate-timescheduledEnd*stringdate-timepickupAt*stringdate-timeserviceLines*array<>externalRef*|The externalRef the request set on the order, null when none.
orderUrl*stringThe customer's Shyni page for this order (cancel, reschedule).
urisourceChannel*"API"Always API for an order created here; the caller cannot choose it.
"API"sourceLabel?|The sanitized sourceLabel stored on the order, or null.
attribution*The attribution stored on the order. Every field is null when it was not sent (all orders before this field existed, and orders from the booking page for the click ids and utmTerm / utmContent).
metadata*The metadata stored on the order; {} when none was sent.
billing*|The company billing details sent as billing when the order was created through POST /api/v1/orders; null when none were sent, and after the customer's data was erased.
paymentUrl?stringThe studio's payment-gateway page for the deposit. Present only when the order awaits a
deposit AND the studio takes online payments; absent otherwise (then the customer pays
through orderUrl). Redirect the customer here; it is a one-customer link, not a secret
of the integrator.
uriholdUntil?stringWhen the slot's hold runs out if the deposit is still unpaid (ISO-8601 instant); absent
when the order holds no slot for a deposit. 30 minutes after creation when the studio
takes card payments through the gateway (the order carries paymentUrl), 3 hours when
the deposit is paid by bank transfer.
date-timeacceptedTermsVersion?integerThe acceptedTermsVersion the order stored; absent when none was sent or the studio has no terms.
waivedDayAdjustment?integerDay-policy surcharge the studio waived on this order (minor units, waiveDayAdjustment on create); absent when nothing was waived.
int64curl -X POST 'https://api.shyni.io/api/v1/orders' \ -H 'Authorization: Bearer shyni_sk_test_YOUR_KEY' \ -H 'Idempotency-Key: 7f3c9a52-1d4e-4b86-a0c7-5e2b9d8f6a13' \ -H 'Content-Type: application/json' \ -d '{ "serviceIds": [ "3b7e9c52-81d4-4f6a-a0c3-2d9e5b8f1a47" ], "scheduledStart": "2026-11-03T08:00:00Z", "scheduledEnd": "2026-11-03T09:30:00Z", "externalRef": "web-2026-000123", "customer": { "name": "Jan Novak", "email": "jan@example.cz", "phone": "+420 777 123 456", "externalRef": "crm-cust-88" }, "variant": "MEDIUM", "subject": { "attributes": { "make": "Skoda", "model": "Octavia", "plate": "1AB 2345" }, "externalRef": "crm-car-17" }, "note": "I will leave the keys in the mailbox."}'Response 201
The created order.
{ "id": "68aa1b98-3c1e-4f0a-9d2b-5e7f1a2b3c4d", "publicId": "84609120", "state": "CREATED", "depositState": "PENDING", "currency": "CZK", "totalPrice": 84200, "depositRequired": true, "depositAmount": 50000, "scheduledStart": "2026-11-03T08:00:00Z", "scheduledEnd": "2026-11-03T09:30:00Z", "pickupAt": "2026-11-03T09:30:00Z", "serviceLines": [ { "name": "Premium hand wash", "price": 84200, "durationMinutes": 90 } ], "orderUrl": "https://app.shyni.cz/order/84609120?token=...", "externalRef": "web-2026-000123", "paymentUrl": "https://gate.gopay.cz/gw/v3/...", "holdUntil": "2026-10-30T10:30:00Z", "sourceChannel": "API", "sourceLabel": null, "attribution": { "utmSource": null, "utmMedium": null, "utmCampaign": null, "utmTerm": null, "utmContent": null, "referrer": null, "gclid": null, "gbraid": null, "wbraid": null, "fbclid": null }, "metadata": {}, "billing": null}Response 400
A required field is missing or a value is invalid, or the studio is not taking online bookings right now. field names the offending input when there is one.
{ "code": "INVALID_INPUT", "message": "customer.email is not a valid e-mail address.", "field": "customer.email"}{ "code": "INVALID_INPUT", "message": "The studio is not accepting online bookings right now (booking intake is paused)."}Response 401
Missing, malformed, unknown, or revoked API key (INVALID_API_KEY).
{ "code": "INVALID_API_KEY", "message": "Missing or invalid API key."}Response 403
The key is valid but the request is refused. Branch on code: INSUFFICIENT_SCOPE (the key lacks the
permission this endpoint requires), PLAN_REQUIRED (the studio is not on a plan that includes API keys,
Pro and above), ORIGIN_NOT_ALLOWED, SECRET_KEY_IN_BROWSER, or DEMO_STUDIO on a write to the demo studio.
See Authentication and Scopes above.
{ "code": "INSUFFICIENT_SCOPE", "message": "This API key does not have the permission this endpoint requires."}Response 409
Several causes share this status, branch on code: CONFLICT with field externalRef
(another order of the studio already carries that externalRef), customer.externalRef or
subject.externalRef (the customer / car the order is attached to already has a different
externalRef, or another customer / car of the studio already owns the one you sent; nothing was
created and the refusal is not stored under the Idempotency-Key), SLOT_UNAVAILABLE (the window is
taken, nothing was created), TERMS_VERSION_STALE (field acceptedTermsVersion: the studio's terms
changed; nothing was created and the refusal is not stored under the Idempotency-Key),
STUDIO_NOT_PUBLISHED (the studio is not published, so it
takes no bookings, a test studio always does; the refusal is not stored under the
Idempotency-Key) and IDEMPOTENCY_IN_PROGRESS (a request under the same
Idempotency-Key is still running, retry shortly).
{ "code": "SLOT_UNAVAILABLE", "message": "The requested time slot is no longer available. Pick another start from GET /api/v1/availability/starts.", "field": "scheduledStart"}{ "code": "TERMS_VERSION_STALE", "message": "The studio's terms changed; show the current version (3) and resend with acceptedTermsVersion = 3.", "field": "acceptedTermsVersion"}{ "code": "STUDIO_NOT_PUBLISHED", "message": "The studio is not published; it cannot take bookings."}{ "code": "CONFLICT", "message": "An order with this externalRef already exists.", "field": "externalRef"}{ "code": "CONFLICT", "message": "The customer matched by this order already has a different externalRef.", "field": "customer.externalRef"}{ "code": "IDEMPOTENCY_IN_PROGRESS", "message": "A request with the same Idempotency-Key is still running. Retry in a moment."}Response 422
Three causes share this status, branch on code: IDEMPOTENCY_KEY_MISMATCH (see Idempotency above),
SERVICE_KNOWN_CARS_ONLY (a selected service has orderAudience KNOWN_CARS and the order proves no known
car with knownCarToken; one answer for a missing, invalid, expired or unmatched token) and
ADDON_SOLD_ONLY_WITH (a selected add-on needs one of its soldOnlyWithServiceIds in the order).
Nothing was created and the refusal is not stored under the Idempotency-Key.
{ "code": "IDEMPOTENCY_KEY_MISMATCH", "message": "This Idempotency-Key was already used for a different request. Use a new key."}{ "code": "SERVICE_KNOWN_CARS_ONLY", "message": "A selected service can only be ordered for a car the studio already knows.", "field": "serviceIds"}{ "code": "ADDON_SOLD_ONLY_WITH", "message": "A selected add-on can only be ordered together with one of the services it is sold with.", "field": "serviceIds"}Response 429
Two causes share this status, branch on code: RATE_LIMITED (a burst or storefront
budget is used up, retry after Retry-After seconds) and PENDING_ORDERS_LIMIT (a
publishable key at a studio that confirms bookings manually; the studio already has 10
unconfirmed orders from the API waiting for its decision, nothing was created, the refusal is
not stored under the Idempotency-Key and there is no Retry-After, retry once the studio
has confirmed or cancelled one).
{ "code": "RATE_LIMITED", "message": "Too many requests. Retry after the time given in the Retry-After header."}{ "code": "PENDING_ORDERS_LIMIT", "message": "The studio has 10 unconfirmed orders from the API waiting for its decision. Try again later."}