Predictable responses
The API uses one success envelope, one error envelope, stable status codes, cursor pagination, and explicit formats for dates and money.
Success and error envelopes
Single resources are returned in data. Lists also containmeta. Every error uses an English machine-readable code and a human-readable message.
{
"data": {
"id": "project_…",
"name": "Mehrfamilienhaus Köln"
}
}{
"error": {
"code": "invalid_body",
"message": "startDate must use YYYY-MM-DD format.",
"details": {}
}
}HTTP status and error codes
| Status | Common codes | What to do |
|---|---|---|
| 400 | invalid_body, invalid_json, invalid_filter, organization_required | Correct the request before retrying. |
| 400 | idempotency_key_required, unsupported_invoice_type | POST /invoices needs an Idempotency-Key header and creates standard, partial and final invoices only. |
| 401 | unauthorized | Create or replace the bearer key. |
| 403 | api_key_paused, insufficient_scope, plan_feature_required, forbidden | Check whether the key is paused, its scope and extra rights, the plan and the organization. |
| 404 | not_found | The route or resource does not exist. |
| 405 | method_not_allowed | Use a method listed in the API reference. |
| 409 | conflict, webhook_limit_reached, endpoint_disabled, document_finalized, document_locked | Resolve the current resource state. |
| 409 | document_not_finalized, document_not_sendable, document_sender_unavailable, document_already_sent | No email was sent. Finalize the document, set a sender mailbox in craftivo or pass resend: true. |
| 409 | pdf_not_ready | No email was sent. Retry after the seconds in Retry-After. |
| 409 | send_outcome_unknown | Do not retry automatically. Check the document's status before you send again. |
| 409 | document_not_sent, invalid_status_transition, document_not_convertible | The customer's answer was not recorded. Send the quote first; an accepted quote cannot be rejected or the other way round. |
| 409 | invalid_source_status, source_not_invoiceable, unsupported_invoice_type | No invoice was created or changed. Use a finalized, sent or accepted quote or order confirmation; change only draft standard invoices. |
| 409 | partial_invoice_conflict | No partial invoice was created. The order forbids it, for example because of a mode other than the one fixed by the first finalized partial invoice; message gives the reason in German. |
| 409 | payment_exceeds_open_amount, payment_not_allowed, external_id_conflict | No payment was booked. Check the open amount and details.reason; a repeated externalId must carry the same data. |
| 409 | final_invoice_conflict | No final invoice was created. An active final invoice already covers the order or project, a partial invoice is still a draft, or the source is not eligible; message gives the reason in German. |
| 422 | idempotency_key_reused | Nothing was booked or created. The Idempotency-Key was already used for a different payment or invoice request; use a new key. |
| 422 | recipient_party_not_on_property | No document was created. The recipient party belongs to another property than the project; change the recipient in craftivo. |
| 422 | customer_required | Set the customer of the quote or source document in craftivo, then create the order confirmation or invoice again. |
| 422 | finalization_incomplete | Add the information listed in details.missingFields in craftivo, then finalize again. |
| 422 | message_required, template_not_found, recipient_email_missing | Pass templateId or subject and body, and a recipient if the document has no delivery address. |
| 502 | send_failed | The mail server rejected the email; nothing was sent. A retry is safe. |
| 502 | send_outcome_unknown | craftivo cannot tell whether the email went out. Check the document before you send again. |
| 429 | rate_limited | Wait for the Retry-After duration. |
| 500 | internal_error | Retry with backoff and keep the request context. |
api_key_paused means the organization owner paused the key in craftivo under Settings > API & Webhooks. Retrying does not help; ask the owner to reactivate it. Requests with a paused key do not count against any rate limit.Cursor pagination
Lists are sorted newest first. Set limit between 1 and 100; the default is 50. Pass the returned cursor to the next request untilisDone is true. A list without matches returns HTTP 200 and an empty array. A page can be shorter than the limit, or empty, whileisDone is false, so do not stop on a short page.
{
"data": [],
"meta": {
"cursor": "cursor_for_next_page",
"isDone": false,
"limit": 50
}
}Dates, timestamps and money
- Resource timestamps use ISO 8601 in UTC, for example
2026-08-09T12:30:00.000Z. A webhook envelope'screatedAtis a Unix timestamp in milliseconds. - Date-only fields use
YYYY-MM-DD. - Monetary amounts are integer cents plus
currency: "EUR". - Public enums and error messages are English.
Idempotency
Send a unique Idempotency-Key with POST and PUT requests. Reusing the same key replays the stored response and addsIdempotency-Replayed: true.
A key applies per API key, method and path; the same key on another path is a new request and leaves the first response in place. Another API key of your organization with the same Idempotency-Key never gets the stored response: its request runs as a new request, under its own permissions. Only successful responses are stored, for 30 days.
Rate limits
Every organization has a daily quota (calendar day in Europe/Berlin, reset at midnight), and every API key has a per-minute limit. Both depend on the plan:
| Access | Requests per day and organization | Requests per minute and key |
|---|---|---|
| Business plan (included) | 10,000 | 120 |
| „Interface“ add-on (Starter/Premium) | 5,000 | 60 |
| Practice account | 2,000 | 60 |
Every response to a recognized API key, including errors, carries these headers. They describe the organization's daily quota. They describe the key's per-minute limit instead on a 429 caused by that limit, and when no organization with API access is known for the request (a legacy key without X-Organization-Id, or a plan without API access).
| RateLimit-Limit | Requests allowed in the current window. |
| RateLimit-Remaining | Requests left in the window after this request. |
| RateLimit-Reset | Seconds until the window resets. |
| Retry-After | Only on 429: seconds to wait before the next request. |
A 429 caused by the per-minute limit asks you to wait until the next minute; a 429 caused by the daily quota until midnight in Berlin. Requests rejected before the quota (404, 405, insufficient_scope, a paused key) do not count against it. The limits are exact, also for parallel requests; under very heavy parallel load of one organization a request can get a 429 with Retry-After: 1. Watch RateLimit-Remaining, and when you get a 429, wait at least the requested duration and add jitter before retrying. At 80 % of the daily quota the organization owner gets one notice per day in craftivo and by email.
File uploads (POST /api/v1/files) have their own quota of 300 uploads per organization and UTC day. A 429 from that quota carriesdetails.quota: "file_uploads" andRetry-After, but no RateLimit-*headers.