Skip to content

Error Codes

All Synapse API errors follow a consistent format with an HTTP status code, a human-readable detail message, and a machine-readable code.


Error Response Format

json
{
"detail": "Human-readable description of the error",
"code": "machine_readable_error_code"
}
FieldTypeDescription
detailstringA sentence describing what went wrong and how to fix it
codestringA stable identifier for programmatic error handling

HTTP Status Codes

StatusMeaningWhen It Occurs
200OKSuccessful read, update, or direct send
201CreatedResource created successfully
202AcceptedEvent accepted and queued for processing
204No ContentResource deleted successfully
400Bad RequestInvalid request body, missing required fields
401UnauthorizedMissing, invalid, or expired authentication credentials
403ForbiddenValid auth but insufficient permissions or plan limits
404Not FoundResource does not exist or is not accessible
409ConflictDuplicate idempotency key or unique constraint violation
422Unprocessable EntityRequest body is valid JSON but fails business validation
429Too Many RequestsRate limit exceeded
500Internal Server ErrorUnexpected server error (reported to Sentry)
502Bad GatewayUpstream service (pyrx.auth, pyrx.payment) unavailable
503Service UnavailableAuthentication service warming up or temporarily unreachable — retryable (see Retry-After)

Error Codes Reference

Authentication Errors (401)

CodeDetailCause
invalid_api_key"API key is invalid or has been revoked"The X-API-KEY header does not match any active key
missing_api_key"X-API-KEY header is required"No API key provided
missing_workspace_id"X-WORKSPACE-ID header is required"No workspace ID provided
missing_token"Missing bearer token"No Authorization: Bearer <token> header was supplied
token_expired"Token has expired"JWT has passed its exp timestamp
invalid_token"Token signature is invalid"JWT was tampered with or signed by an unknown key

Authorization Errors (403)

A 403 means your credentials are valid but not permitted for this operation. Which code you receive depends on why the request was denied — API-key scope, RBAC permission, JWT role, or plan limits. See the note below the table for the two response shapes these codes use.

CodeDetailCause
missing_permission"Missing permission: <permission>"The caller's role (or SDK data-plane API key) lacks the specific permission this endpoint requires — for example events:write or flows:write. The most common 403. Carries required_permission.
insufficient_scope"API key requires scopes […] but only has […]"The API key's scope (data, reporting, management, full) does not cover this endpoint. Carries required_scopes and your_scopes.
scope_forbidden"API key scope does not include <scope>"A data-plane write endpoint (contacts, devices, identify, alias) was called by an API key whose scope does not include the required write scope. Carries required_scope.
insufficient_role"This endpoint requires one of roles […] but your role is <role>"A dashboard (JWT) request was made by a user whose role is not permitted for this endpoint. Carries required_roles and your_role.
control_plane_only"This operation is only available from the dashboard"An API key was used against a dashboard-only (control-plane) endpoint. Carries a dashboard_url deep link.
workspace_mismatch"API key does not belong to the specified workspace"Key belongs to a different workspace than X-WORKSPACE-ID
plan_limit_reached"Email limit reached for your plan. Upgrade to send more emails."Monthly email, contact, flow, member, or API key limit exceeded
workspace_suspended"Workspace is suspended due to payment failure"Workspace is in read-only mode
Note

403 responses use one of two body shapes. Permission-style codes (missing_permission, scope_forbidden) return the standard envelope with the identifier under code. The scope/role/plane family (insufficient_scope, insufficient_role, control_plane_only) returns an envelope with error: "forbidden" and the identifier under reason, plus the relevant required_* / your_* fields. When handling 403s programmatically, read code first and fall back to reason.

Validation Errors (400)

CodeDetailCause
validation_errorMissing required fieldA required field is missing from the request body
invalid_emailValue is not a valid email addressEmail field fails format validation
invalid_event_name"Event name must be lowercase snake_case"Event name contains invalid characters
invalid_filter_operatorUnknown filter operatorSegment filter uses an unsupported operator
invalid_date_param"from_date/to_date must be ISO-8601…"422from_date/to_date on GET /v1/flows/{id}/goal-conversions is not valid ISO-8601 (URL-encode a + offset as %2B)

Not Found Errors (404)

CodeDetailCause
contact_not_found"No contact found with the given identifier"Contact ID or external_id does not exist
flow_not_found"Flow not found"Flow ID does not exist or is archived
template_not_foundTemplate not found or is inactiveTemplate slug does not exist or was soft-deleted
segment_not_found"Segment not found"Segment ID does not exist

Conflict Errors (409)

CodeDetailCause
duplicate_event"Event with this idempotency key has already been processed"Same idempotency_key submitted twice
duplicate_external_id"A contact with this external_id already exists"POST /v1/contacts with an existing external_id
duplicate_slug"A template with this slug already exists"POST /v1/templates with an existing slug
goal_event_name_conflict"A goal for event '…' already exists on this flow"Creating or renaming a conversion goal to an event_name already used by another goal on the same flow

Rate Limit Errors (429)

CodeDetailCause
rate_limit_exceededRate limit exceeded. Retry after N seconds.Too many requests in the current window

Server Errors (5xx)

CodeDetailCause
internal_error"An unexpected error occurred"Unhandled server exception (logged to Sentry)
auth_unavailable"Authentication service is warming up. Please retry."Returned as a retryable 503 when the pyrx.auth JWKS fetch fails or the auth circuit breaker is open (e.g. a cold cache during a fresh deploy, or pyrx.auth temporarily unreachable). Carries a Retry-After header — retry after the advertised delay.
payment_service_unavailable"Payment service temporarily unavailable"pyrx.payment API unreachable

Handling Errors

Python

python
response = requests.post(url, headers=headers, json=data)
 
if response.status_code == 202:
event = response.json()
elif response.status_code == 409:
# Duplicate -- treat as success
pass
elif response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
time.sleep(retry_after)
else:
error = response.json()
print(f"Error {response.status_code}: {error['code']} - {error['detail']}")

JavaScript

javascript
const response = await fetch(url, { method: "POST", headers, body });
 
if (response.status === 202) {
const event = await response.json();
} else if (response.status === 409) {
// Duplicate -- treat as success
} else if (response.status === 429) {
const retryAfter = parseInt(response.headers.get("Retry-After") || "60");
await new Promise((r) => setTimeout(r, retryAfter * 1000));
} else {
const error = await response.json();
console.error(`Error ${response.status}: ${error.code} - ${error.detail}`);
}
Tip

Always check the code field for programmatic error handling. The detail message may change between API versions, but code values are stable.