Guide
Errors
The API uses conventional HTTP status codes and returns every error in the same JSON envelope, with a stable machine-readable code.
{
"error": {
"type": "permission_error",
"code": "insufficient_scope",
"message": "This API key is missing the records:write scope. Create a key with the required scopes.",
"details": { "required_scopes": ["records:write"], "missing_scopes": ["records:write"] },
"request_id": "req_4fJ2tq9cXbW1Lm0a"
}
}| Property | Meaning |
|---|---|
type | Broad category of the error (below). |
code | Stable, machine-readable code. Branch on this. |
message | Human-readable explanation. It may change; do not parse it. |
param | The parameter or field that caused the error, when known. |
details | Extra structured context, such as validation issues or missing scopes. |
request_id | Unique id for the request. |
Every response, successful or not, has an X-Request-Id header, and errors repeat it as error.request_id. Log it with failures and include it when you contact support.
| Type | Status | Meaning |
|---|---|---|
invalid_request_error | 400, 405, 413, 415, 422 | The request is malformed, or the write was rejected by validation or workflow rules. |
authentication_error | 401 | The API key is missing, invalid, revoked, or expired, or its user or workspace is inactive. |
permission_error | 403 | The key lacks a scope, or its user lacks permission. |
not_found_error | 404 | The object type, record, or route does not exist, or the user cannot read it. |
conflict_error | 409, 412 | A conflicting change, an in-progress idempotent request, or a stale If-Match. |
rate_limit_error | 429 | Too many requests for this key. |
api_error | 500, 503 | A problem on our side. |
New types may be added. Treat an unknown type according to its HTTP status.
| Code | Status | Meaning |
|---|---|---|
invalid_json | 400 | The request body is not valid JSON. |
validation_failed | 400 | Parameters or body failed validation; see param and details. |
invalid_filter | 400 | A filter, sort, or field selection is invalid. |
invalid_cursor | 400 | The pagination cursor is malformed or belongs to a different query. |
missing_api_key | 401 | No Authorization: Bearer <key> header was sent. |
invalid_api_key | 401 | The key is malformed, unknown, or does not verify. |
api_key_revoked | 401 | The key was revoked. |
api_key_expired | 401 | The key passed its expiry date. |
api_key_user_inactive | 401 | The key's user is deactivated. |
api_key_tenant_inactive | 401 | The workspace is suspended or not yet open. |
developer_access_revoked | 403 | The key's user no longer holds API Keys → create (developer access). |
tenant_host_mismatch | 403 | The key belongs to a different workspace than the hostname. |
insufficient_scope | 403 | The key lacks the scope this endpoint requires. |
permission_denied | 403 | The key's user lacks the object, record, or field permission. |
search_not_supported | 403 | Listing and searching this object type is disabled for the workspace. |
write_not_supported | 403 | The object does not support this write through the API. |
object_not_available | 404 | Unknown object type, or the object is not exposed through the API. |
record_not_found | 404 | No record with this id that the caller can read. |
route_not_found | 404 | No endpoint matches this method and path. |
user_not_found | 404 | No workspace member with this id that you may read. |
workflow_not_found | 404 | No workflow with this id in the workspace. |
comments_not_supported | 404 | Records of this object type have no comment thread. |
method_not_allowed | 405 | The path exists but not for this HTTP method. |
conflict | 409 | Unique values already exist, a referenced record is missing or still referenced, or records changed concurrently. |
idempotency_key_in_use | 409 | A request with this Idempotency-Key is still in progress. |
stale_record | 412 | If-Match did not match the record's current version. |
payload_too_large | 413 | The request body exceeds 1 MB. |
unsupported_media_type | 415 | Send Content-Type: application/json. |
invalid_write | 422 | The write was rejected by field, owner, or workflow validation. |
idempotency_key_reused | 422 | The Idempotency-Key was used with a different request. |
rate_limited | 429 | Too many requests for this key; retry after Retry-After seconds. |
internal_error | 500 | Unexpected server error. Retry with backoff and report the request_id. |
service_unavailable | 503 | A dependency (vault, database) is temporarily unavailable. |
Some endpoints return more specific codes, such as workflow_not_found, user_not_found, comments_not_supported, search_not_supported, and conflict. Each endpoint's statuses are listed in the API reference. New codes may be added at any time.
| Response | Retry? |
|---|---|
429 rate_limited | Yes, after the Retry-After header. |
500, 503 | Yes, with exponential backoff and jitter. Retry writes only with an Idempotency-Key. |
409 idempotency_key_in_use | Yes, after a short delay, with the same key. |
412 stale_record | Re-read the record, re-apply your change, then retry. |
Other 4xx | No. Fix the request first; retrying the same request will fail the same way. |
const API = 'https://api.preshos.com/v1';
async function preshos(path, init = {}, attempt = 0) {
const response = await fetch(`${API}${path}`, {
...init,
headers: {
Authorization: `Bearer ${process.env.PRESHOS_API_KEY}`,
'Content-Type': 'application/json',
...init.headers,
},
});
const body = await response.json();
if (response.ok) return body;
const { error } = body;
const retryable =
response.status === 429 ||
response.status >= 500 ||
error.code === 'idempotency_key_in_use';
if (retryable && attempt < 5) {
const retryAfter = Number(response.headers.get('Retry-After'));
const backoff = Math.min(30_000, 500 * 2 ** attempt) * (0.5 + Math.random() / 2);
await new Promise((resolve) => setTimeout(resolve, retryAfter > 0 ? retryAfter * 1000 : backoff));
return preshos(path, init, attempt + 1);
}
throw Object.assign(new Error(error.message), {
status: response.status,
code: error.code,
requestId: error.request_id,
});
}