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=…",
"previous": null
}
nextandpreviousare opaque URLs. Follow them 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": []
}
}
detailsis present on every 4xx — a list of field-level failures, empty when the error has no field breakdown. Don’t index into it unconditionally on a 5xx.paramappears only when exactly one field is at fault.
There are nine 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 |
|
402 |
|
|
403 |
|
|
404 |
also returned instead of 403 for records outside your company — no existence leak |
|
409 |
state conflicts: |
|
429 |
carries |
|
5xx |
our side: |
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.
The same holds for timesheets: POST …/timesheets/<id>/approve/ on a row that is already approved is a 409 already_approved, and POST …/timesheets/<id>/confirm/ without a body on hours a person already confirmed is a 409 already_confirmed (on an approved row, confirm/ is a 409 locked). Both mean the row is already where you wanted it — refetch it rather than retrying.
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
The two resources treat an omitted scope differently:
shifts— omit it on a series and you get a409 delete_scope_requiredrather than a guess about what you meant.availability— the scope defaults toall: omitting it deletes the whole block.singleandfollowingadditionally require anoccurrence=<ISO datetime>anchor naming the occurrence you mean; a missing anchor is a400.
Rate limits#
A 429 carries a Retry-After header. Honour it, and back off exponentially on repeats.