Reliability and limits
What is safe to retry, what happens when you send the same thing twice, and which limits stop a call. Read this before you put an integration into production.
Repeating a call
Holofin has no Idempotency-Key header. Whether a repeat is safe depends on the endpoint, as listed below.
| Call | Safe to repeat? | What a repeat does |
|---|---|---|
| POST /workflow-runs/ | No | Creates a second run and consumes credits again. There is nothing in the payload Holofin deduplicates on, not even an identical document. Track the run_id on your side before retrying. |
| POST /cases/ | Yes | Merge semantics keyed on case_id. Only the keys you send are touched, an explicit null clears one, and re-sending identical values changes nothing: no revision, no re-evaluation. |
| POST /workflow-runs/{run_id}/cancel | Harmless | Nothing changes, but a run that is already completed, failed or cancelled answers 409 CANNOT_CANCEL. Treat that 409 as success, not as an error to retry. |
| PATCH and DELETE | Yes | Webhook and organization updates settle on the state you sent. |
| All GET endpoints | Yes | Read-only. |
A timed-out POST /workflow-runs/ may still have created the run. Before resending, list runs with created_after set just before your call and match on document_name. List items are trimmed: metadata is only on GET /workflow-runs/{run_id}, so fetch a candidate by ID to confirm it is yours.
Retrying failures
| Status | Retry? |
|---|---|
| 400, 409, 413, 415 | Never. The request or the resource state is wrong and will stay wrong. Fix it. |
| 401, 403 | Never. The key is missing, inactive, expired, or the caller's IP is not on the key's allow-list. |
| 402 | Not until credits are topped up. Retrying burns nothing but will keep failing. |
| 404 | Never for a resource in another workspace. A run you just created is visible immediately, so a 404 there means the ID is wrong. |
| 429 | For the per-key rate limit, after the Retry-After seconds. For the sandbox allowance, backing off does not help. See limits below. |
| 5xx | Yes, with exponential backoff, but only for calls the table above marks safe to repeat. |
Every error code and its payload
Limits
Limits that stop a call:
| Limit | Signal | What to do |
|---|---|---|
| Request rate | 429, Retry-After | 100 requests per minute per API key, across all authenticated endpoints. Wait the number of seconds in Retry-After. Use webhooks rather than tight polling loops. |
| Document size | 400 PAGE_LIMIT_EXCEEDED, 413 | 200 pages per document, 100 MB per request body (base64 adds a third to the file size). |
| Credit balance | 402 INSUFFICIENT_CREDITS | Watch usage and alert before you hit zero. Every credit type counts toward the same balance. |
| Sandbox allowance | 429 SANDBOX_LIMIT_EXCEEDED | A sandbox workspace has a fixed credit allowance in total, not per period. Backing off does not help; the allowance has to be raised. |
| Page size on lists | limit, max 100 | Higher values are clamped rather than rejected. Page with next_token. |
| IP allow-list on a key | 403 | Set per key. Add your egress addresses in developer settings before moving hosts. |
Webhook delivery
Deliveries retry on your behalf, so your receiver only needs to be idempotent and fast:
- Any
2xxcounts as delivered. Anything else is retried. - Up to 7 attempts over 48 hours, with exponential backoff plus jitter.
- An endpoint is marked
FAILINGafter 5 consecutive failures, and keeps receiving events. - Because deliveries retry, your handler will see the same event more than once. Key it on
run_idand the event type.
Acknowledge first, process afterwards. A receiver that does the work before returning 200 turns a slow database into a duplicate delivery.
Pagination
List endpoints are cursor-paginated, not offset-paginated. You get a next_token; you pass it back. There are no page numbers and no total count, which is what keeps paging stable while new runs are being created underneath you.
Walk every page
TOKEN=""
while :; do
PAGE=$(curl -s -G "https://app.holofin.ai/api/v1.1/workflow-runs/" \
-H "X-API-Key: $HOLOFIN_API_KEY" \
--data-urlencode "limit=100" \
${TOKEN:+--data-urlencode "next_token=$TOKEN"})
echo "$PAGE" | jq -r '.runs[].run_id'
TOKEN=$(echo "$PAGE" | jq -r '.pagination.next_token // empty')
[ -z "$TOKEN" ] && break
done| Parameter | Description |
|---|---|
| limit | Page size, default and maximum 100. Larger values are clamped. |
| next_token | Opaque cursor, returned as pagination.next_token. Omit it for the first page; it is absent from the response once pagination.has_next is false. Do not parse it or build one yourself. |
| workflow_id | Only runs of that workflow. |
| status | completed, failed or cancelled. |
| created_after, created_before | ISO 8601 bounds. The pair a reconciliation job wants. |
Listing only returns terminal runs. A run still pending or running does not appear, so do not poll a listing to track progress. To follow one run, fetch it by ID; to be told when any run finishes, use webhooks.