Errors
The Trybe API uses conventional HTTP status codes to signal the result of a request, and returns a JSON body describing the failure. Every error response is machine-readable; you should never need to scrape an HTML page or parse a free-form string to recover.
Response shape
Most errors return a single-field envelope:
{
"message": "A description of the error"
}
Validation errors add an errors map keyed by field name. Each field maps to an
array of human-readable messages — multiple rules can fail at once on the same
field:
{
"message": "The request didn't pass validation",
"errors": {
"email": ["The email must be a valid email address."],
"starts_at": [
"The starts_at field is required.",
"The starts_at must be a date after now."
]
}
}
The message field is always present and always a string. errors is only
present on 422 responses. No other fields are guaranteed across error types —
don't read them.
Status codes
Trybe uses the standard HTTP status code set. Successful responses fall in the
2xx range; failures in 4xx (your side) or 5xx (our side).
| Status | Name | When you see it |
|---|---|---|
200 OK |
Success | The request returned a body. |
201 Created |
Created | A POST created a new resource. The response body is the new resource. |
204 No Content |
Success, no body | A DELETE or some PUT/PATCH operations. There is no response body. |
400 Bad Request |
Bad request | The request was malformed — invalid JSON, a missing required body, or a parameter the server couldn't parse. |
401 Unauthorized |
Unauthenticated | No token, an invalid token, or the token was revoked. |
403 Forbidden |
Forbidden | The token is valid, but the user doesn't have permission for this action on this resource. |
404 Not Found |
Not found | The path doesn't exist, or the resource ID doesn't exist (or isn't visible to the caller). |
409 Conflict |
Conflict | The request couldn't be applied because of a state conflict — e.g. attempting to cancel an order that's already settled. Not every endpoint uses this; check the endpoint reference. |
422 Unprocessable Entity |
Validation failed | The request was syntactically fine, but one or more fields broke validation rules. |
5xx |
Server error | Something on our side broke. Retry with backoff; if it persists, contact support. |
We do not currently enforce a rate-limit error code (429); see
rate limits for the current posture and the backoff
patterns you should build anyway.
Concrete error bodies
The exact JSON for each error type, taken from the OpenAPI definitions you can test against in any environment.
400 Bad Request
{
"message": "The request was malformed."
}
Triggered by syntactically invalid input — broken JSON, an unparseable date, or a query parameter the server can't read. The body is informational; don't write code that branches on its text.
401 Unauthorized
{
"message": "Unauthenticated"
}
The token is missing, malformed, or no longer valid. See authentication for the header shape and the most common causes.
403 Forbidden
{
"message": "This action is unauthorized."
}
The token authenticates a real user, but that user doesn't have permission for the requested action on the requested resource. Permissions in Trybe are per-site; the most common cause is that the integration user hasn't been added to a new site yet.
404 Not Found
{
"message": "The requested resource could not be found"
}
Returned for two cases that look identical from the outside: the resource genuinely doesn't exist, or it exists but the caller can't see it. This is intentional — leaking the difference would help an attacker enumerate IDs.
422 Unprocessable Entity
{
"message": "The request didn't pass validation",
"errors": {
"guests": ["The guests must be at least 1."],
"starts_at": ["The starts_at field is required."]
}
}
Use errors to render messages next to the right form fields. The keys match
the field names in the request body (or in query parameters for GET filter
validation).
5xx
{
"message": "Server error"
}
Trybe occasionally returns a 5xx while we recover from a downstream issue.
Retry idempotent requests (any GET, plus PUT and DELETE calls) with
exponential backoff. For non-idempotent operations (POSTs that create
resources), check whether the resource was actually created before retrying —
many POST endpoints accept an idempotency hint or expose a follow-up GET
you can use to confirm state.
Handling errors
The same pattern works everywhere: check the status, branch on it, and pull the message (and any field errors) out of the body.
async function listSites() {
const res = await fetch('https://api.try.be/sites', {
headers: {
Authorization: `Bearer ${process.env.TRYBE_API_KEY}`,
Accept: 'application/json',
},
})
if (res.ok) {
return res.json()
}
const body = await res.json().catch(() => ({ message: 'Unknown error' }))
switch (res.status) {
case 401:
throw new AuthError(body.message)
case 403:
throw new ForbiddenError(body.message)
case 404:
throw new NotFoundError(body.message)
case 422:
throw new ValidationError(body.message, body.errors ?? {})
default:
throw new TrybeError(`${res.status}: ${body.message}`)
}
}
class ValidationError extends Error {
constructor(message, fields) {
super(message)
this.fields = fields // { [fieldName]: string[] }
}
}
The same shape in PHP:
try {
$response = $client->post('/shop/appointments', ['json' => $payload]);
$data = json_decode((string) $response->getBody(), true);
} catch (GuzzleHttp\Exception\ClientException $e) {
$status = $e->getResponse()->getStatusCode();
$body = json_decode((string) $e->getResponse()->getBody(), true);
if ($status === 422) {
foreach ($body['errors'] as $field => $messages) {
$form->addError($field, $messages[0]);
}
return;
}
throw new TrybeException("$status: {$body['message']}", $status, $e);
}
Designing for resilience
A few patterns to bake in from day one:
- Branch on status, not on
message. The HTTP status code is the contract. The message text may improve over time and isn't versioned. - Surface validation errors to users field-by-field. The
errorsmap is designed for this; rendering onlymessagefor a422discards useful information. - Retry
5xxand network errors with exponential backoff and jitter. Cap the number of retries — three is usually plenty — and treat repeated 5xx as signal to alert rather than retry harder. - Never retry a
4xxwithout changing the request. It will fail the same way the second time. - Log the response body on unexpected errors. Redact
Authorizationheaders, but keep the response body —messageis the fastest way to diagnose what your code sent wrong.
See also
- Authentication — what causes
401and403. - Pagination — list responses use the same JSON envelope shape as everything else.
- Rate limits — current behaviour around
429.