signelloAPI

Conventions

The response envelope, pagination, rate limits and every error code.

The envelope

Every response, success or failure, is a JSON object with an ok field.

Success
{ "ok": true, "data": { "…": "…" } }
Failure
{ "ok": false, "error": "invalid-cursor" }

Check ok before you touch data. error is a stable machine-readable code from the list below; it is not a sentence and it will not change wording on you. There is no human-readable message field, because the string you show your own users is yours to write.

List endpoints put the page beside the envelope rather than inside it:

A page of documents
{ "ok": true, "data": [/* … */], "nextCursor": "…" }

Timestamps and money

Every timestamp is ISO 8601 in UTC, for example 2026-09-03T08:14:22.000Z. Convert to Europe/Stockholm yourself if you are showing it to someone.

Amounts are objects: { "amount": 184500, "currency": "SEK" }. The amount is in the currency's main unit, not minor units, so that example is 184 500 kr. It can carry decimals, and it is always excluding VAT, whichever way the sender chose to show the price in the document itself.

A document's value comes from its pricing block, so one without a price has value: null. Subscriptions count as the whole contract value, not the monthly amount.

Pagination

GET /documents is cursor-paginated and returns newest first.

ParameterDefaultNotes
limit25Between 1 and 100.
cursornoneThe previous response's nextCursor.
statusnoneOne of draft, sent, viewed, signed, declined, expired.

Pass nextCursor back as cursor to get the next page. When nextCursor is null, you have reached the end.

Walking every page
let cursor = null;
const documents = [];

do {
  const url = new URL("https://www.signello.se/api/v1/documents");
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.SIGNELLO_API_KEY}` },
  });
  const body = await res.json();
  if (!body.ok) throw new Error(body.error);

  documents.push(...body.data);
  cursor = body.nextCursor;
} while (cursor);

Do not build cursors yourself or hold on to one for days. A cursor you did not get from us comes back as invalid-cursor.

Rate limits

Each key gets a token bucket: 120 requests up front, refilling at 1 per second, which is 3 600 an hour sustained. The bucket refills continuously rather than resetting on the hour, so there is no window boundary to pile up against, and a quiet key is back to a full burst on its own.

Two headers ride along on every response:

HeaderMeaning
X-RateLimit-LimitThe burst size, 120.
X-RateLimit-RemainingWhole requests left in the bucket right now.

When the bucket is empty you get 429 with rate-limited and a Retry-After header in seconds. Wait that long, then continue. Normal integration traffic does not reach this: an initial sync of a few hundred documents fits inside one burst.

If you find yourself polling hard enough to worry about the limit, you probably want webhooks instead.

Errors

CodeStatusMeaning
invalid-api-key401Missing, malformed or revoked key.
plan-required403The workspace is not on Signello Max.
rate-limited429Bucket empty. See Retry-After.
invalid-query400A query parameter is out of range or not recognised.
invalid-cursor400The cursor was not one we issued.
invalid-id400The id in the path is not a UUID.
not-found404No such record in this workspace.
invalid-json400The request body is not valid JSON.
invalid-body400The body parsed but failed validation.
invalid-url400A webhook URL that is not a valid URL.
unsafe-url400A webhook URL we refuse to call. See Webhooks.
limit-reached409The workspace already has 10 webhook endpoints.

A 404 is also what you get for a document that exists in someone else's workspace. The API never confirms that a record exists somewhere you cannot see.

On this page