Skip to main content
Every error response is { "error": "...", "details"?: {...} }. Read error first: it names the cause. See Errors and rate limits for the full status table.

400 Bad Request

  • Invalid JSON body. The body could not be parsed. Check for trailing commas, unquoted keys or an empty body.
  • The action does not fit the resource’s state, such as sending a package that is not a DRAFT, or sending from a template whose workflow still has a sender question unanswered. The message says which.

401 Unauthorized

The API could not accept the key. The body is always { "error": "Unauthorized" }, so check each of these:
  • No Authorization header, a header that is not Bearer <key>, or a token that does not start with fsk_.
  • Unknown key. Keys are shown once; if the value is lost, create a new one at Settings > API keys.
  • Revoked or expired key. Revocation is immediate, and a key with an expiry stops working once that moment passes. Create a new key.
  • The key’s role lost the API access permission. An administrator can restore it from Roles & Permissions.
  • Wrong X-Workspace-Id. The header may only repeat the key’s own workspace id. Omit it, or read the id from GET /api/v1/workspaces. To act in another workspace, create a key for it.

402 Payment Required

  • The plan does not include the API (or webhooks, on /api/v1/webhooks routes, or custom fields, on /api/v1/custom-fields). These are Enterprise plan features; the message names the missing one.
  • No credit left to send. Sending charges one package against the organisation’s allowance.
  • A plan limit would be exceeded, such as the number of recipients on a package or the number of templates. The message says which.

403 Forbidden

  • Missing permission: ... names the permission the key’s role lacks, such as canSendPackages (or Missing permission: one of ... when any of several would do). Grant it from Roles & Permissions, or issue a key on a role that has it.
  • Insufficient template access: ... required. The key’s role has a lower template access level than the call needs. Raise it from Roles & Permissions.
  • IP address not allowed. The organisation has an IP allowlist (Settings > Security) and the request came from outside it. Check the address your request actually leaves from: a cloud function or a CI runner is not your office.
  • This action requires a signed-in session, not an API key. The route is an admin route for signed-in users only, not part of the public API.

404 Not Found

The resource does not exist in the workspace the key acts in, or the key’s role cannot see it. A resource in another workspace is a 404, not a 403: create a key for that workspace.

409 Conflict

The request conflicts with the resource’s current state:
  • Another member is editing the package or template in the app. Documents and fields cannot be changed through the API while that edit lock is held, and a locked draft cannot be sent. Try again once they are done.
  • Void on a package that has already ended, on hold on one that is not IN_PROGRESS, or resume on one that is not ON_HOLD.
  • Send before every file has arrived. PUT each document’s bytes to its uploadUrl before sending; a package with no documents is refused the same way.
  • Download before completion. Signed copies and the certificate exist only once the package has completed.
  • Duplicate externalId. Another package already carries it. On POST /api/v1/packages/from-template, repeating the externalId of a package the key can see returns that package instead.
  • 50 active keys. An organisation can hold at most 50; revoke one to make room.

413 Payload Too Large

POST /api/v1/templates/{templateId}/documents refuses a document whose fileSize is over the per-document limit. GET /api/v1/packages/{packageId}/download refuses documents too large to merge into one PDF; use GET /api/v1/packages/{packageId}/download/zip instead.

422 Unprocessable Entity

The JSON was well-formed but failed validation. details maps each failing field path to its messages: recipients.0.email is the email of the first recipient, and details.pageSize on a GET means a query parameter. Unknown merge field, metadata or role keys produce the same shape.

429 Too Many Requests

All /api/v1 routes share one limit of 120 requests per 60-second window, counted per source IP address. Wait at least the Retry-After seconds before retrying, and prefer webhooks over polling.

502 Bad Gateway

POST /api/v1/templates/{templateId}/documents/{documentId}/convert returns 502 when the document conversion service fails. Retry with backoff; if it persists, contact support@flowsign.app.

Upload URL expired

An uploadUrl is single-use and expires after two hours. If the PUT fails, add the document again with POST /api/v1/packages/{packageId}/documents (or the template equivalent) and upload to the new URL.

Webhook not arriving

  • The endpoint is Failing. After 10 consecutive failed deliveries the endpoint is skipped for new events. Resume it in Settings > Webhooks or with PATCH /api/v1/webhooks/{endpointId} and { "enabled": true }. A paused endpoint receives nothing either.
  • Signature mismatch. Sign the x-flowsign-timestamp header, a ., and the raw request body with HMAC-SHA256, then compare with each v1= entry in x-flowsign-signature. Use the raw bytes, not a re-serialised body: re-serialising changes whitespace and key order, so the digest no longer matches. See Webhooks.
  • Private address refused. Endpoints must be public https:// URLs. A private, loopback or link-local address is refused at registration, and a delivery whose host resolves that way is exhausted without retries.
  • Slow or redirecting responses. Respond 2xx within 10 seconds; a 3xx counts as a failure because redirects are not followed.
The delivery log in Settings > Webhooks, or GET /api/v1/webhooks/{endpointId}, shows each attempt’s status and error.

Network or CORS error from a browser

The API is meant to be called from your server, not from a web page. A request made directly from browser code fails on CORS before it reaches Flowsign, and it would expose your key to anyone who can open the page. Call the API from your backend and have the page talk to that.