Errors & conventions

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.

Success
{
  "data": {
    "id": "project_…",
    "name": "Mehrfamilienhaus Köln"
  }
}
Error
{
  "error": {
    "code": "invalid_body",
    "message": "startDate must use YYYY-MM-DD format.",
    "details": {}
  }
}

HTTP status and error codes

StatusCommon codesWhat to do
400invalid_body, invalid_json, invalid_filter, organization_requiredCorrect the request before retrying.
400idempotency_key_required, unsupported_invoice_typePOST /invoices needs an Idempotency-Key header and creates standard, partial and final invoices only.
401unauthorizedCreate or replace the bearer key.
403api_key_paused, insufficient_scope, plan_feature_required, forbiddenCheck whether the key is paused, its scope and extra rights, the plan and the organization.
404not_foundThe route or resource does not exist.
405method_not_allowedUse a method listed in the API reference.
409conflict, webhook_limit_reached, endpoint_disabled, document_finalized, document_lockedResolve the current resource state.
409document_not_finalized, document_not_sendable, document_sender_unavailable, document_already_sentNo email was sent. Finalize the document, set a sender mailbox in craftivo or pass resend: true.
409pdf_not_readyNo email was sent. Retry after the seconds in Retry-After.
409send_outcome_unknownDo not retry automatically. Check the document's status before you send again.
409document_not_sent, invalid_status_transition, document_not_convertibleThe customer's answer was not recorded. Send the quote first; an accepted quote cannot be rejected or the other way round.
409invalid_source_status, source_not_invoiceable, unsupported_invoice_typeNo invoice was created or changed. Use a finalized, sent or accepted quote or order confirmation; change only draft standard invoices.
409partial_invoice_conflictNo 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.
409payment_exceeds_open_amount, payment_not_allowed, external_id_conflictNo payment was booked. Check the open amount and details.reason; a repeated externalId must carry the same data.
409final_invoice_conflictNo 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.
422idempotency_key_reusedNothing was booked or created. The Idempotency-Key was already used for a different payment or invoice request; use a new key.
422recipient_party_not_on_propertyNo document was created. The recipient party belongs to another property than the project; change the recipient in craftivo.
422customer_requiredSet the customer of the quote or source document in craftivo, then create the order confirmation or invoice again.
422finalization_incompleteAdd the information listed in details.missingFields in craftivo, then finalize again.
422message_required, template_not_found, recipient_email_missingPass templateId or subject and body, and a recipient if the document has no delivery address.
502send_failedThe mail server rejected the email; nothing was sent. A retry is safe.
502send_outcome_unknowncraftivo cannot tell whether the email went out. Check the document before you send again.
429rate_limitedWait for the Retry-After duration.
500internal_errorRetry 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.
An unknown path returns 404. A known path called with an unsupported method returns 405. Keep these cases separate in logs and monitoring.

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.

List metadata
{
  "data": [],
  "meta": {
    "cursor": "cursor_for_next_page",
    "isDone": false,
    "limit": 50
  }
}

Dates, timestamps and money

  • Resource timestamps use ISO 8601 in UTC, for example2026-08-09T12:30:00.000Z. A webhook envelope'screatedAt is 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:

AccessRequests per day and organizationRequests per minute and key
Business plan (included)10,000120
„Interface“ add-on (Starter/Premium)5,00060
Practice account2,00060

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-LimitRequests allowed in the current window.
RateLimit-RemainingRequests left in the window after this request.
RateLimit-ResetSeconds until the window resets.
Retry-AfterOnly 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.