Skip to content
API pagesConventions

API documentation

Conventions

The rules on this page apply to every route. The resource pages assume you have read it.

Requests and responses

  • JSON in, JSON out. Send Content-Type: application/json on any request with a body.
  • Property names are camelCase in both directions.
  • URLs are lowercase, and query string values keep their case.
  • Query string keys are matched without regard to case, so ?page=2 and ?Page=2 are the same request. Write them camelCase, the way this documentation does. The published OpenAPI document spells the filter and paging keys with a leading capital, because they are bound from a model whose property names it prints. A client you generate from it follows the document, and both spellings reach the same parameter.
  • Four routes take multipart form data instead of JSON: POST /v1/documents, which takes profileId and file, and the three avatar uploads, which take file on its own. One route returns a ZIP file and not JSON: see Exports and handover packs.
  • Unknown properties in a request body are ignored rather than refused.
  • A null property and an absent property mean the same thing on almost every route. Where they do not, the endpoint says so.

Enums are numbers

Enums serialize as integers, not as names. "status": 3 on a document means Processed.

{ "status": 3, "sourceProviderType": 0 }

The one exception is actor on GET /v1/status, which is a lowercase string: anonymous, session, apikey, mcp, or system.

Every enum used in a response is listed with its numbers on the page that returns it. Treat the numbers as stable: they are part of the contract, and new members are added at the end.

Dates and time zones

Shape Format Example Used for
Timestamp ISO 8601, UTC 2026-09-11T14:30:00Z Anything ending in At
Date YYYY-MM-DD 2026-09-04 Issue dates, due dates, trend points
Month YYYY-MM 2026-09 Periods and filters named period
Time of day HH:MM:SS 09:00:00 Reminder send windows

Every timestamp the API returns is UTC. Send UTC.

Two places are not UTC, and both say so in their response:

  • Reminder send windows are local to the organization's time zone. GET /v1/reminders/sending-today returns timeZoneId, localDay, dayStartUtc, and dayEndUtc together so you can line them up.
  • A period is a calendar month in the organization's reckoning, not a UTC window.

Date ranges are inclusive at both ends. from=2026-09-01&to=2026-09-30 includes both days. Which date a range filters on depends on the route, and it is not always the same one:

  • GET /v1/invoices filters on the invoice issue date.
  • GET /v1/documents filters on the date the document arrived.

Each page says which.

Money and currency

  • Amounts on invoices are decimal numbers, not minor units: 1190.0 means 1,190.00.
  • currencyCode is an ISO 4217 code, for example RON or EUR. There is no implied currency. Do not assume every invoice for one client is in the same one.
  • Affiliate commissions are the exception: they are integer minor units in amountMinorUnits and totalMinorUnits, with currencyCode beside them. See Affiliates.
  • The API does no currency conversion. What a supplier billed is what you get.

Locale

  • locale is a language tag such as en or ro. Organizations, client companies, and contacts each carry one.
  • The API does not translate its own responses. Errors carry a messageKey and you resolve it, or you show your own text. See the error shape.
  • Locale affects what Syllevo sends to other people: a reminder email goes out in the contact's locale, not the caller's.

Pagination

List routes that can grow take page parameters and return a page envelope.

Request:

Parameter Type Default Notes
page integer 1 1-based. Clamped to 1 at the low end
pageSize integer 25 Clamped to 200 at the high end, 1 at the low end

Response:

{
  "items": [],
  "page": 1,
  "pageSize": 25,
  "totalCount": 0,
  "totalPages": 0
}

Out-of-range values are clamped rather than refused, so a pageSize of 5000 returns 200 items and a page of 0 returns page 1. A page past the end returns an empty items array, which is the signal to stop.

There are no cursors. Sort order is stable enough to page through a month, but an insert during a long walk can shift a row between pages. For an exact snapshot, use an export (Exports and handover packs) rather than paging.

Some list routes return a plain JSON array with no envelope, because the list is bounded by nature: connections, contacts, members, API keys, and similar. Those take no page parameters.

Filtering

  • Filters are query parameters and combine with AND.
  • An omitted filter means no filtering on that field, not a default value. The exception is period on completeness and reminder routes, where omitting it has a documented meaning.
  • profileId narrows a firm-wide list to one client company. Without it you get every client the credential can see, which for a firm-wide key is the whole portfolio.
  • A filter that names something the credential cannot see answers 403, not an empty list.

Sorting

Only GET /v1/documents and GET /v1/documents/facets take sort parameters, sortBy and sortDirection. See Documents.

Idempotency

The Idempotency-Key header

Routes that take something in for good, where a retry must not file it twice, require an Idempotency-Key header. Today that is one route: POST /v1/profiles/{profileId}/sales-invoices. Each such route says so on its own page. Every other route ignores the header.

Idempotency-Key: postify-sale-2026-000481
  • Pick one key per thing you mean to do, and reuse it only to retry that same thing. The id of the record on your side is a good key. Up to 255 visible ASCII characters.
  • Same key, same request: you get the first answer back, status and body, without anything being done again. The response carries Idempotent-Replayed: true, and the first response's Location header when it had one.
  • Same key, different request: 422 idempotency_key_reused. The key is already spent on something else. Use a new one.
  • Same key while the first request is still running: 409 idempotency_request_in_progress with Retry-After: 2. Wait, then retry with the same key.
  • No key, or an unusable one: 400 idempotency_key_required.

Keys belong to whoever sent them: an API key's keys never collide with another API key's, or with a person's. Only successful answers are kept. A request that failed with a 4xx or a 5xx can be retried with the same key and runs again. Keys are forgotten after 30 days.

Without the header

Everywhere else, safety comes from the operations themselves:

  • Uploads deduplicate on content. POST /v1/documents hashes the file. The same bytes for the same client return the existing document with "duplicate": true and answer 200 instead of 201. Nothing is stored twice.
  • Email ingestion deduplicates on the message. A replayed message answers 202 with "replayed": true.
  • Several state changes are already idempotent. Archiving an archived client, deleting an avatar that is already gone, and marking a read SPV message read all succeed and return current state rather than failing.
  • Some are not. Creating a client company, an invitation, or an API key twice creates two. Retry those only after a network failure with no response, and check for the duplicate.

Retry a 429, a 500, or a transport failure. Do not retry a 4xx other than 429: the request will fail the same way.

The error shape

Every failure a Syllevo handler produces returns this body:

{
  "status": 409,
  "code": "supplier_merge_already_official",
  "messageKey": "web.errors.suppliers.mergeAlreadyOfficial",
  "detail": null,
  "details": null,
  "errors": null
}
Field Type Always Notes
status integer yes Repeats the HTTP status
code string yes A stable machine identifier. Branch on this
messageKey string yes The translation key for a human-facing message
detail string no Non-null only in development builds. Never rely on it
details object no Extra structured data on a few codes, for example an availableAt time
errors object no Field-level validation problems, keyed by field name

Branch on code, never on messageKey or detail. The codes are listed in Error codes.

Two responses do not use this shape:

  • Framework refusals. A 401 from the default-deny policy, including the one an unmatched path produces, is an RFC 9457 problem document with type, title, status, detail, and instance. See Authentication.
  • One typed refusal. POST /v1/profiles/{profileId}/reminders/send-now answers 409 with every field above plus pendingTouchAt, so the caller learns when the already-scheduled reminder goes out. See Reminders.

Status codes

Status Means
200 Done. Also the answer to a duplicate upload and to a delete that returns the current entity
201 Created. The body is the new thing, and a Location header says where to read it back
202 Accepted, not yet done. Work runs on the Worker. Used by source syncs, email ingestion, and refund requests
204 Done, nothing to return
400 The request is malformed or a value is out of range. code is validation_failed unless a more specific code applies
401 No credential, or one that is not valid. The response does not say which. Also what a wrong path answers
402 The organization's package does not cover this. See the paywall
403 The credential is valid but not for this organization, this client, or this action
404 The thing does not exist, or exists somewhere this credential cannot see. The two are not distinguished
409 The request is well formed but the current state refuses it. code says which rule
413 The body is larger than the route allows. See size limits
429 Rate limited. See rate limits
500 Something failed inside the API. Nothing was silently dropped: the request is in the logs under its correlation id
503 An upstream service Syllevo depends on is unreachable. Only GET /v1/registry/company returns this

422 is not used. Validation failures are 400.

404 is used where the row is absent and 409 where the row exists but refuses. For example, asking for the file of a document whose original is missing from storage is 409 document_original_missing, not 404, because the document row is right there and retrying the same request would re-sign the same absent key forever.

The paywall

An organization without an active package can still reach a small set of routes, so it can pay, get its data out, and see where it stands. Everything else answers:

{
  "status": 402,
  "code": "entitlement_product_access_required",
  "messageKey": "web.errors.entitlement.productAccessRequired",
  "detail": null
}

Reachable on any method while the wall is up:

  • /v1/billing/*
  • /v1/exports/*
  • /v1/status
  • /v1/onboarding/session

Reachable on GET and HEAD only:

  • /v1/organization/*
  • /v1/organizations
  • /v1/users/*

Export stays open unconditionally. A firm that stops paying can always take its documents with it.

A 402 is not a permissions problem and retrying will not fix it. Read GET /v1/billing for the current state. See Billing.

Rate limits

Every request passes one global limiter. Fixed one-minute windows, no queueing: over the limit is an immediate 429, not a wait.

Partition Limit per minute Applies to
Per API key 600 Any request carrying an API key
Per organization 1,200 Session requests that resolved an organization
Per IP address 200 Everything else, including unauthenticated calls

A few routes carry a tighter limit of their own, on top of the global one:

Route Limit per minute Partitioned by
GET /v1/search 120 Organization
GET /v1/registry/company 20 Caller
POST /v1/billing/sync 20 Organization
GET and POST /v1/uploads/{token} 30 IP address

POST /v1/ingestion/email and POST /v1/billing/webhooks/stripe are carved out of the global limiter entirely and carry their own. See Webhooks.

A 429 carries no Retry-After header today. The window is one minute, so retry after a minute with jitter.

If you are pulling a large portfolio, page steadily rather than in parallel bursts. 600 requests a minute on one key is generous for a nightly sync and easy to blow through with 50 concurrent workers.

Request size limits

Route Whole body One file
POST /v1/documents 26,000,000 B 25,000,000 B
POST /v1/profiles/{profileId}/sales-invoices 51,000,000 B 25,000,000 B
POST /v1/uploads/{token} 26,000,000 B 25,000,000 B
POST /v1/organization/avatar 6,000,000 B 5,000,000 B
POST /v1/profiles/{id}/avatar 6,000,000 B 5,000,000 B
POST /v1/users/me/avatar 6,000,000 B 5,000,000 B

The numbers are decimal, so 25,000,000 bytes is the "25 MB" the upload dialog prints. The extra million bytes on the whole body is slack for multipart framing, so a file exactly at the limit is not refused for the envelope around it.

Over the limit answers 413. Inside a multi-file upload link, one oversized file is reported as rejected in the response and the rest still land. See Upload links.

Correlation ids

Send X-Correlation-Id on any request and Syllevo echoes it on the response and stamps it on every log line the request produces. Omit it and the API generates one and still echoes it.

X-Correlation-Id: 7f9a1d2c-0000-4000-8000-000000000001

When something goes wrong, the correlation id is what makes it findable. Log it on your side and quote it in support.

GET /v1/status returns the correlation id it resolved, which is a quick way to prove the header is getting through your own proxies.

Versioning and deprecation

  • The version is a path segment: /v1/.... There is no version header and no Accept negotiation.
  • Responses carry an api-supported-versions header listing the versions the deployment serves.
  • Additive changes happen inside v1 without notice: new routes, new optional request fields, new response properties, and new members at the end of an enum. Write clients that ignore properties they do not know.
  • Breaking changes do not happen inside v1. A v2 would appear beside it, and v1 would keep answering.
  • Deprecations are announced in Changelog before anything stops working.

CORS

Browser calls are allowed only from an explicit allowlist of origins, configured per deployment, with credentials enabled. Calling the API directly from a customer's browser with an API key is not the intended shape, and it would put your key in a page anyone can read. Call it from your server.