Integration API

Send orders in from your own system.

A website, a kiosk, a register that isn't on Square yet, a spreadsheet with a Zapier automation — anything that can make an HTTP request can put an order on your board. Send a name or a ticket number and it appears instantly; your staff pick it up, mark it ready, and hand it off exactly like any other order.

Get your key

From your dashboard, open a shop and find Custom integration. Click Generate key — your key (it starts with psk_) is shown exactly once, so copy it somewhere safe right away, like a password manager. There's no way to view it again later, only replace it: generating a new key immediately stops the old one from working. You can also revoke a key without generating a new one, from that same panel.

The endpoint

Every order is one authenticated POST request.

RequestPOST https://pickupsignal.com/api/ingest-order
HeaderAuthorization: Bearer psk_your_key_here
HeaderContent-Type: application/json

Full example

A real order, ready to copy and run — swap in your own key and shop id.

curl -X POST https://pickupsignal.com/api/ingest-order \
  -H "Authorization: Bearer psk_DeiyfU6z9m0nmhpexfPet7lLVdT2wVhpwoFirNkhIoc" \
  -H "Content-Type: application/json" \
  -d '{
        "shopId": "Kx3Yq9dLmPa2Rv8Tw4Zn",
        "name": "Priya",
        "drink": "Taro Milk Tea",
        "source": "online",
        "id": "order-10432"
      }'

Response — 200

{"num": 47, "orderId": "i2a91cf6b8d4e4a2b9f7d3c1a5e6b8f90"}

Running a ticket-number board instead of names? Send ticket in place of name: {"shopId": "Kx3Yq9dLmPa2Rv8Tw4Zn", "ticket": 47, "id": "order-10433"}.

Fields

Every request body needs a shopId and at least one of name or ticket. Everything else is optional.

FieldTypeRequiredConstraints
shopIdstringYes Your shop's id — shown in the Custom integration panel alongside your key, with its own Copy button.
namestringName or ticket Up to 120 characters. Shown on the board as the order's name; extra whitespace is collapsed.
ticketintegerName or ticket 0–999999. Used for a numbered-ticket board instead of a name. Send both and the ticket number wins for numbering — the name is still kept on the order.
drinkstringNo Up to 120 characters. Shown alongside the name or ticket on the board.
idstringNo, recommended Up to 128 characters. Your own unique reference for this order — makes the request safely repeatable. See Retries & idempotency below.
sourcestringNo One of instore, online, app. Defaults to online when omitted.

Responses & errors

A created order returns its board number and id. Every rejection returns a short error code you can branch on.

StatuserrorMeaning
200Created. Body: {"num", "orderId"}.
200Duplicate replay of the same id — see below. Body: {"duplicate": true, "num", "orderId"}, matching the original order.
400name or ticket requiredNeither name nor ticket was sent.
400ticket out of rangeticket wasn't a whole number from 0 to 999999.
400invalid sourcesource wasn't instore, online, or app.
400invalid idid was present but empty, not a string, or over 128 characters.
400shop-timezone-unsetThe shop has no timezone on file and the request had no ticket — see below.
401unauthorizedMissing or wrong Authorization header, wrong shopId for that key, or a revoked key.
404shop-not-foundThe shop no longer exists.
405method-not-allowedSomething other than POST.
429rate limitedOver 60 requests/minute for this shop. A Retry-After: 30 header says how long to wait.
500service-unavailableRetry later; if it persists, regenerate your key or contact support.
About shop-timezone-unset: a ticket-less order needs to know your shop's calendar day to hand out the right number, so we never guess it. Choosing a manual-entry mode during onboarding, or generating your key from the dashboard, sets it automatically — after that this error won't come up. Sending a ticket with every request sidesteps it entirely.

Retries & idempotency

Include id on every request and you can never create the same order twice: send the same id again on the same business day and you'll get back the original order (duplicate: true, the same num and orderId) instead of a new one. That scoping is per day, not forever — reuse order-42 again tomorrow for a different order and it's treated as new.

Why this matters for Zapier/Make: both platforms commonly retry a webhook step automatically when it times out or the connection drops. A retry is indistinguishable from a fresh request unless you send id — map a stable, unique field from your trigger (an order number, a row id, anything that identifies that specific order) into id, and a retried delivery lands as the same order instead of a duplicate on your board.

A Zapier or Make recipe, no code needed

The same five steps whether you're wiring up Zapier's Webhooks action or Make's HTTP module.

  1. Trigger. Whatever starts a new order in your existing tool — a new row in a spreadsheet, a new order in your online store, a new form submission.
  2. Action. Add Webhooks by Zapier (Zapier) or an HTTP module (Make), set to POST, URL https://pickupsignal.com/api/ingest-order.
  3. Headers. Authorization: Bearer psk_your_key_here and Content-Type: application/json.
  4. Field mapping. Build the JSON body from your trigger's fields — shopId, name (or ticket), optionally drink, and id mapped to something unique from the trigger so retries stay safe.
  5. Test safely. Run it once by hand with a fixed body like {"shopId": "your-shop-id", "name": "Test Order", "id": "test-1"}. Watch it land on your board, then open the staff page and tap Remove from queue on that order before turning the automation on for real — it asks you to confirm, then gives you 6 seconds to Undo if you tap it by mistake.

Rate limit

60 requests per minute, per shop. Go over it and you'll get a 429 with a Retry-After: 30 header — wait 30 seconds and try again. Ordinary order volume, even a busy counter, comes nowhere close; the limit exists to catch a runaway integration, not to slow down real traffic.

Orders appear on your board instantly — staff advance them like any other order.