Conventions#
These hold across the whole surface. Reading this once saves rediscovering it per resource.
Versioning#
The version is in the path: /api/public/v1/.
Additive changes — new fields, new endpoints, new optional parameters, new enum values — ship inside v1. Your client must tolerate them, which takes two habits: ignore response fields you don’t recognise, and don’t fail closed on an unfamiliar enum value.
Removals, renames, retypes, and anything that tightens validation bump the version, with an overlap window during which both are live.
List responses#
Lists are cursor-paginated and always come back in the same envelope:
{
"object": "list",
"data": [ { "object": "shift", "id": "0194d1c0-…", "…": "…" } ],
"has_more": true,
"next": "https://app.shiftavo.com/api/public/v1/shifts/?cursor=…"
}
nextis an opaque URL. Follow it as-is; do not construct or parse cursors.Stop when
has_moreisfalse.page_sizecontrols the page length.
The object discriminator#
Every resource carries an "object" field naming its type — "shift", "location", "employee". Branch on it rather than on the shape of the payload, especially inside webhook handlers and mixed collections.
Errors#
One envelope, everywhere:
{
"error": {
"type": "conflict_error",
"code": "not_cancellable",
"message": "…",
"details": [],
"param": "start_date"
}
}
detailsis always present — a list of field-level failures, empty when the error has no field breakdown.paramappears only when exactly one field is at fault.
There are seven types. Each maps to one status except invalid_request_error, the catch-all for the non-tabulated 4xx, where the code carries the distinction:
|
Status |
Notes |
|---|---|---|
|
400 · 405 · 406 · 415 |
|
|
400 |
|
|
401 |
missing / invalid / expired token |
|
403 |
|
|
404 |
also returned instead of 403 for records outside your company — no existence leak |
|
409 |
state conflicts: |
|
429 |
carries |
Branch on type for handling class, on code for the specific case. Both are stable; message is not — treat it as human-readable only.
Idempotency#
Send an Idempotency-Key header on creates. A retried request then replays the first result instead of creating a second record.
Reusing a key with a different body is a 400 idempotency_key_reused — the key identifies one request, not one endpoint.
Request tracing#
Every response, success and error alike, carries an X-Request-Id header. Log it. Send your own X-Request-Id and it is echoed back, so a trace id can travel end to end. Quote it in any support request.
Lifecycle transitions#
A transition fires as a POST to an action sub-path: POST …/<id>/publish/, …/cancel/, …/revoke/.
A transition that cannot apply — cancelling a draft, publishing something already published — is a 409 with a specific code (not_cancellable, not_publishable, not_unpublishable). It is never a 200 that silently did nothing.
Deleting#
DELETE returns a stub, not a 204:
{ "id": "0194d1c0-…", "object": "shift", "deleted": true }
A DELETE never takes a body. Everything a delete needs is in the path or the query string, so a generated client can express all of them. (HTTP leaves a DELETE body’s semantics undefined and OpenAPI 3.0 tells consumers to ignore one, so a body field here would silently disappear from your client and come back as a 400 the schema couldn’t explain.)
Delete is not the soft verb#
Where a resource offers both, they are not synonyms, and the wrong choice is not recoverable:
Resource |
|
The soft verb |
|---|---|---|
|
removes the shift; assignees of a published shift are notified and any live marketplace offer is withdrawn first |
|
|
terminal removal: access revoked, the person leaves every collection and a later |
|
|
only while pending or declined — a confirmed or revoked request carries a ledger booking, so it is a |
|
Recurring records#
Deleting one occurrence of a recurring shifts or availability record needs an explicit scope:
DELETE …/<id>/?scope=this|following|all # shifts
DELETE …/<id>/?scope=single|following|all # availability
Omit it on a series and you get a 409 delete_scope_required rather than a guess about what you meant.
Rate limits#
A 429 carries a Retry-After header. Honour it, and back off exponentially on repeats.