---
title: "Errors and limits"
description: "Rate limits, the error bodies of the key check, and how a well-behaved client polls."
---

> Documentation Index
> Fetch the complete documentation index at: https://developer.unipage.be/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and limits

## Rate limits

Per Core API key:

| Limit | Over it |
|---|---|
| 60 calls per minute | `429` with `{"error": "rate_limited_minute", "retry_after": <seconds>}` |
| 1 call per second | `429` with `{"error": "rate_limited", "retry_after": 1}` |

The wait is in the response body (`retry_after`, in seconds); there is no
`Retry-After` header. Wait at least that long before the next call.

## Errors from the key check

| Status | Body | Meaning |
|---|---|---|
| `401` | the standard unauthorized response, no `error` key | No key was sent, or a Storefront key (`upg_sf_...`) was used: those never work here. |
| `401` | `{"error": "Malformed API key"}` | The key has no secret part (no dot). |
| `401` | `{"error": "Invalid API key"}` | Unknown key, wrong secret, or a key of another location. |
| `403` | `{"error": "API key disabled"}` | The restaurant switched the key off. |
| `429` | see above | Too many calls. |

Errors from an endpoint itself are Laravel's standard JSON: a `422` with
`{"message": "...", "errors": {"field": ["..."]}}` for invalid input, a `404`
for an unknown id.

## Behaving well

Your client runs unattended, often on a schedule. A few rules keep it (and
the restaurant's till, which uses the same API) healthy:

- **Never retry a 4xx in a tight loop.** A 401, 403, 404 or 422 will answer
  the same on the next try. Log it, alert a human, and stop.
- **Back off on 429** for `retry_after` seconds, and spread your calls: one
  call per second is the hard ceiling.
- **Sync incrementally.** Use the cursors the endpoints offer
  (`synced_until` on sales, `updated_after` on customers and products)
  instead of downloading everything again.
- **Prefer webhooks for new sales.** A [`pos.sale` webhook](/core/webhooks/)
  pushes every new receipt the moment the till syncs it, so you only poll to
  catch up.
- **Ignore fields you do not know.** Responses gain new keys over time;
  existing keys are never renamed or removed.

## Ids and times

- Ids in responses are short hash ids (`x94d3`), not database numbers. Use
  them as opaque strings.
- `created_at` and `updated_at` style fields are Unix timestamps in seconds,
  unless the endpoint says otherwise. Ticket dates are date-time strings as
  the till recorded them.

Source: https://developer.unipage.be/core/errors-and-limits/index.mdx
