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.

CallSafe to repeat?What a repeat does
POST /workflow-runs/NoCreates 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/YesMerge 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}/cancelHarmlessNothing 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 DELETEYesWebhook and organization updates settle on the state you sent.
All GET endpointsYesRead-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

StatusRetry?
400, 409, 413, 415Never. The request or the resource state is wrong and will stay wrong. Fix it.
401, 403Never. The key is missing, inactive, expired, or the caller's IP is not on the key's allow-list.
402Not until credits are topped up. Retrying burns nothing but will keep failing.
404Never for a resource in another workspace. A run you just created is visible immediately, so a 404 there means the ID is wrong.
429For the per-key rate limit, after the Retry-After seconds. For the sandbox allowance, backing off does not help. See limits below.
5xxYes, 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:

LimitSignalWhat to do
Request rate429, Retry-After100 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 size400 PAGE_LIMIT_EXCEEDED, 413200 pages per document, 100 MB per request body (base64 adds a third to the file size).
Credit balance402 INSUFFICIENT_CREDITSWatch usage and alert before you hit zero. Every credit type counts toward the same balance.
Sandbox allowance429 SANDBOX_LIMIT_EXCEEDEDA 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 listslimit, max 100Higher values are clamped rather than rejected. Page with next_token.
IP allow-list on a key403Set 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 2xx counts as delivered. Anything else is retried.
  • Up to 7 attempts over 48 hours, with exponential backoff plus jitter.
  • An endpoint is marked FAILING after 5 consecutive failures, and keeps receiving events.
  • Because deliveries retry, your handler will see the same event more than once. Key it on run_id and the event type.

Acknowledge first, process afterwards. A receiver that does the work before returning 200 turns a slow database into a duplicate delivery.

The full retry schedule

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
ParameterDescription
limitPage size, default and maximum 100. Larger values are clamped.
next_tokenOpaque 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_idOnly runs of that workflow.
statuscompleted, failed or cancelled.
created_after, created_beforeISO 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.