Errors
The APROS API error shape, the HTTP status codes and error codes you can receive, how to report a server fault with its errorId, and how to retry safely with Idempotency-Key.
When a request fails, the API answers with a non-2xx HTTP status and a JSON body in one shape:
{
"error": "Permission denied: VIEW_INVOICE required",
"code": "ACCESS_DENIED"
}| Field | Type | When it is present |
|---|---|---|
error | string | Always. A sentence in English for people and logs. Do not parse it: the wording can change. |
code | string | When the API refuses on purpose: a missing permission, a validation rule, a plan limit, a switched-off module. Stable, so your code can branch on it. |
details | object | With some codes, to say exactly what was wrong (for example which query parameter or which field). |
errorId | string | On server faults (500). The same ID is in the server log next to the cause. |
Some refusals carry one more field named in the table below, such as issues for validation
errors or requiredPermission for an access-level refusal.
Handle errors in your code
- Check the HTTP status first. A
2xxis success; anything else carries the shape above. - If the body has a
code, branch on it. Codes are the contract; messages are not. - If there is no
code, act on the status. A few endpoints still answer some400and404responses without one. - On
500, log theerrorIdso you can report it.
Status codes
| Status | Meaning |
|---|---|
400 Bad Request | The request is malformed or failed validation. Fix it before you retry. |
401 Unauthorized | No API key, or the key is unknown, revoked or expired. |
403 Forbidden | The key is valid but may not do this: a missing scope or permission, a switched-off module, a plan limit, or a disabled account. |
404 Not Found | The record or path does not exist in your workspace. |
409 Conflict | The request conflicts with the current state, for example a reused Idempotency-Key. |
429 Too Many Requests | The key went over its rate limit. Wait for the number of seconds in the Retry-After header. See Rate limits. |
500 Internal Server Error | A fault on the APROS side. The body has an errorId. |
503 Service Unavailable | The workspace cannot serve this request right now, for example during an update. Retry later. |
Error codes
These codes come from the API's shared error handling, so you can meet them on any endpoint.
Individual endpoints add their own codes, such as STAGE_NOT_FOUND on POST /api/leads; the
error text explains each one.
| Status | code | What happened | Extra fields |
|---|---|---|---|
| 400 | VALIDATION_ERROR | The request body failed validation. | issues: one entry per problem, each with the field's path and a message. |
| 400 | UNKNOWN_QUERY_PARAM | A query parameter the endpoint does not accept. The endpoint refuses it rather than ignore it, so you never get an unfiltered list that looks filtered. | details.unknownParams, details.acceptedParams |
| 400 | DUPLICATE_QUERY_PARAM | A query parameter appears more than once where the endpoint takes one value. | details.duplicatedParams |
| 400 | INVALID_QUERY_PARAM | A query parameter has a value the endpoint cannot use. | details.param, details.message |
| 400 | UNKNOWN_CUSTOM_FIELD_FILTER | A cf_ filter names a custom field that does not exist. | details.unknownParams |
| 400 | INVALID_ID | An ID in the path or in a filter is not a valid ID. | |
| 400 | INVALID_REFERENCE | The body refers to a record that does not exist in your workspace. | details.field |
| 400 | INVALID_CURRENCY | A currency code that is unknown or not active in your workspace. | |
| 400 | IDEMPOTENCY_KEY_INVALID | The Idempotency-Key header is longer than 255 characters. | |
| 401 | AUTHENTICATION_ERROR | No valid API key. | |
| 403 | ACCESS_DENIED | The key's scopes, or its owner's role, do not include the permission. The message names what is missing. | |
| 403 | ACCESS_LEVEL_NONE | The key's owner has no access level for this action on this kind of record. | requiredPermission: the permission to grant the role, or to add to the key's scopes. |
| 403 | ACCOUNT_DISABLED | The person the key belongs to is suspended or terminated, so their keys no longer work. | |
| 403 | MODULE_DISABLED | The module this endpoint belongs to is switched off for your workspace. | Can include key (the module) and reason: plan if your plan does not include it, self if an administrator switched it off. |
| 403 | QUOTA_EXCEEDED | A plan limit is reached. Only creating new records is refused; existing data stays readable. | details.quota, details.used, details.limit |
| 404 | NOT_FOUND | The record does not exist in your workspace. | |
| 409 | IDEMPOTENCY_KEY_REUSED | This Idempotency-Key was already used with a different request body. | |
| 409 | IDEMPOTENCY_KEY_IN_FLIGHT | The first request with this Idempotency-Key is still running. | |
| 429 | RATE_LIMITED | The key went over its rate limit. | Retry-After response header, in seconds |
| 503 | SCHEMA_OUT_OF_DATE | Your workspace is being updated and this part is not ready yet. |
A validation error looks like this:
{
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"issues": [
{
"code": "invalid_value",
"values": ["HOT", "WARM", "COLD"],
"path": ["temperature"],
"message": "Invalid option: expected one of \"HOT\"|\"WARM\"|\"COLD\""
}
]
}A request body that is not valid JSON answers 400 with { "error": "Invalid JSON body" }.
Server faults and errorId
A 500 means something failed on the APROS side, not in your request. The body never explains
the cause; it carries a short ID instead:
{ "error": "Failed to fetch leads", "errorId": "e_3f9a1c" }The same errorId is written to the server log next to the real cause. To get the fault
investigated, write to hello@apri-code.com and include:
- the
errorId, - your workspace address,
- the method and path you called, and roughly when.
You do not need to send your API key or the request body.
Retrying safely
Retry only what can succeed on a second try:
| Response | Retry? |
|---|---|
429 | Yes, after the number of seconds in Retry-After. |
500, 503, or a network error with no response | Yes, with backoff (for example 1 s, 2 s, 4 s). |
Any other 4xx | No. The same request will fail the same way; fix it first. |
Retrying a create is risky: if the first request succeeded but its response was lost, a retry
creates a second record. To prevent that, send an Idempotency-Key header with a value that is
unique for the operation, such as a UUID, and reuse the same value when you retry:
curl -X POST "https://<your-workspace>.apros.app/api/tasks" \
-H "Authorization: Bearer $APROS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c9a52-8d3e-4b7a-a2f0-9c4d1e8b7a63" \
-d '{ "title": "Call back Koval Studio" }'How APROS treats the header:
- Same key, same body: APROS returns the stored response of the first successful request. The operation does not run again.
- Same key, different body:
409withIDEMPOTENCY_KEY_REUSED. Use a new key for every distinct operation. - Same key while the first request is still running:
409withIDEMPOTENCY_KEY_IN_FLIGHT. Nothing was created twice; retry later with the same key. - After a failure: only successful (
2xx) responses are stored. If the first attempt failed, the key is free, and a retry with it runs the operation again. - Keys are kept for 24 hours and belong to the person the API key belongs to.
- Without the header, the endpoint behaves as usual and does no replay protection.
These endpoints accept Idempotency-Key:
| Endpoint | Operation |
|---|---|
POST /api/leads | Create a lead |
POST /api/leads/{id}/convert | Convert a lead |
POST /api/contacts | Create a contact |
POST /api/companies | Create a company |
POST /api/deals | Create a deal |
POST /api/activities | Create an activity |
POST /api/crm/comments | Add a CRM comment |
POST /api/projects | Create a project |
POST /api/tasks | Create a task |
POST /api/tasks/{id}/checklist | Add a checklist item |
POST /api/tasks/{id}/comments | Add a task comment |
POST /api/invoices | Create an invoice |
POST /api/invoices/{id}/duplicate | Duplicate an invoice |
POST /api/invoices/{id}/payment-link | Create a payment link |
POST /api/invoices/{id}/payments | Record a payment |
POST /api/invoices/{id}/payments/{paymentId}/reverse | Reverse a payment |
POST /api/invoices/{id}/send | Send an invoice |
The Booking API is a separate public API for booking widgets, and its errors use a different shape. Its page lists them.