Conventions
Response envelope, error format, and cursor pagination.
Every /v1 response follows the same shape, regardless of resource. Once you've integrated one endpoint, you've integrated all of them.
Response envelope
Section titled “Response envelope”Successful responses wrap the payload in data and carry pagination hints in meta:
{
"data": { "status": "ok" },
"meta": {
"cursor": null,
"hasMore": false
}
}data— the resource or list of resources. Never null on success.meta.cursor— opaque string; pass it to the next request to continue paginating.nullwhen there's no next page.meta.hasMore—trueif more pages exist.
Errors
Section titled “Errors”Errors return a structured body regardless of HTTP status:
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid Bearer token.",
"details": {}
}
}code— stable machine-readable identifier ("unauthorized","not_found","rate_limited","invalid_argument", etc.). Safe to switch on.message— human-readable string. Good for logs; not safe to show verbatim in end-user UIs without translation.details— optional, per-code. For validation errors, contains a field-keyed map of specifics.
Common HTTP statuses:
| Status | Meaning |
|---|---|
| 200 | Success |
| 400 | Invalid request (body shape, missing required fields) |
| 401 | Missing / invalid / revoked token |
| 403 | Authenticated but not authorized (User key without permissions) |
| 404 | Resource does not exist, or belongs to another organization |
| 409 | State conflict (exceeded a limit, duplicate) |
| 429 | Rate limit exceeded (Rate limits) |
| 5xx | Server error — treat as retryable with backoff |
Pagination
Section titled “Pagination”List endpoints accept two query parameters:
limit— integer, default20, max100. Values above100are clamped silently.cursor— opaque string from a previous response'smeta.cursor. Omit on the first request.
Pagination is forward-only and cursor-based — there are no page numbers. The cursor encodes sort position; it is stable across new records being added.
GET /v1/clients?limit=50
GET /v1/clients?limit=50&cursor=eyJpZCI6MTIzfQWhen meta.hasMore is false, stop paginating.
Request limits
Section titled “Request limits”Text fields are length-bounded server-side. Oversized input is rejected with 400 invalid_argument (never a 5xx); the error message names the field that was too long, so you can shorten it and retry. Current caps:
| Field | Limit |
|---|---|
Note content (POST /clients/{clientId}/notes) |
50,000 characters |
Note subject |
500 characters |
Lengths are counted in Unicode characters (code points), not bytes.
Multi-tenancy
Section titled “Multi-tenancy”Every request is scoped to the organization that owns the Bearer key. Objects that belong to a different organization return 404 (not 403) — Salfio does not leak the existence of other orgs' data.