Skip to content
API pagesSources

API documentation

Sources

A source is a route documents arrive by: a mailbox invoices are forwarded to, an ANAF e-Factura connection, a supplier portal login, a connected drive. The API calls a source an account.

Connecting a source is meant to be a one-time act of under two minutes, after which nobody opens Syllevo again for that client. These routes are how an integration does that act.

For what arrives, see Documents. For the ANAF connection specifically, see e-Factura and SPV.

Routes

Method Path Needs
GET /v1/profiles/{profileId}/accounts View
GET /v1/profiles/{profileId}/accounts/{id} View
POST /v1/profiles/{profileId}/accounts ManageSources
PUT /v1/profiles/{profileId}/accounts/{id} ManageSources
DELETE /v1/profiles/{profileId}/accounts/{id} ManageSources
POST /v1/profiles/{profileId}/accounts/{id}/sync ManageSources
POST /v1/profiles/{profileId}/accounts/sync ManageSources
GET /v1/accounts View, across the portfolio
GET /v1/connectors Any credential
POST /v1/connectors/{slug}/connect ManageSources
POST /v1/connectors/{slug}/callback ManageSources
POST /v1/connectors/{slug}/credentials ManageSources
GET /v1/connectors/accounts View
DELETE /v1/connectors/accounts/{accountId} ManageSources
GET /v1/ingestion/unfiled View
POST /v1/ingestion/unfiled/{id}/resolve ManageSources

The account routes are nested under the client company on purpose. Permissions are held on the client, so the client id belongs in the URL where the router can require it, and an account id from somewhere else cannot be addressed at all: the API proves the account belongs to the client in the path before it answers anything.

The account shape

Field Type Means
id uuid The source
profileId uuid The client company
providerType DataProviderType What kind of source it is
tag string or null Free-text label, for example office inbox
syncStatus AccountSyncStatus How it is doing
lastSuccessfulSyncAt date-time, null Last time it worked
lastErrorCode string or null Why it last failed. See Error codes
ingestionAddress string or null The address suppliers send to. Mail sources only

providerType:

Value Name Means
0 EmailInbox A mailbox invoices are sent to
1 EFacturaAnaf ANAF e-Factura
2 SupplierPortal A supplier connector
3 CloudStorage A connected drive
4 ManualUpload Uploads by hand and upload links

syncStatus:

Value Name Means
0 NeverSynced Connected, nothing has run yet
1 Healthy Working
2 Degraded Working, but something went wrong recently
3 Failing Not working. lastErrorCode says why
4 Disconnected The credential is gone. It needs reconnecting

There is no config on the response, and there never will be. Credentials are encrypted before they reach the database and do not come back out, not to the web app, not to an API key, not to the person who typed them.

ingestionAddress is the one thing about a source that does come back, and the contrast is the point. A credential is something only its holder may see. An inbound address is a mailbox printed on screen precisely so it can be handed to strangers, and hiding it would make the product's central instruction, "send your invoices here", unanswerable.

GET /v1/profiles/{profileId}/accounts

Every source on this client. View. Answers a plain array.

Parameter Type Means
tag string Optional. Narrow to sources with that label

View is the gate rather than ManageSources, because seeing that a client has an inbox connected is part of seeing the client. Changing one is the other question, and every change here asks ManageSources.

GET /v1/accounts

The same rows, across every client the caller can see, paged. A firm with three hundred clients would otherwise need three hundred calls to draw one grid.

Parameter Type Means
profileId uuid Narrow to one client
tag string Narrow to one label. ?tag= is no filter
page integer Default 1
pageSize integer Default 25, clamped to 200

Both filters narrow within what the caller can already see. Naming a client the caller cannot see is an empty page, not a 403, so a foreign client id and an id nobody ever created give the same answer. An all-zero GUID is refused with 400.

This route is not a second door onto one client's sources. That door is GET /v1/profiles/{profileId}/accounts, and it stays the only place a single client is authorized by name.

POST /v1/profiles/{profileId}/accounts

Connects a source. ManageSources.

Field Type Required Means
providerType DataProviderType yes What kind of source
tag string no A label
config object no Connection settings and credentials

config is shaped by whichever provider owns the providerType: an IMAP host and password, a portal login, and so on. It is encrypted before it reaches storage and has no counterpart on the response.

Answers 201 with the account. For a mail source, ingestionAddress on that response is the address to give the client's suppliers.

Each organization has a cap on how many sources of each kind it can hold. Passing it answers 409 with source_cap_reached. The e-Factura and manual-upload caps are one per client, and that one is a correctness constraint rather than a fair-use limit.

PUT /v1/profiles/{profileId}/accounts/{id}

Retags a source, and rotates its credentials when a config is sent. ManageSources.

Field Type Means
tag string Sent as null to clear it. This is a PUT, what arrives is what you get
config object Omit to keep the stored credentials. Send it to replace them wholesale

The asymmetry between the two is deliberate. Credentials are never returned, so a client cannot read the current value, change one field, and send the whole thing back the way it can with a tag. If config were required here, every rename would mean re-typing a password.

DELETE /v1/profiles/{profileId}/accounts/{id}

Disconnects a source. ManageSources. Answers 204.

Nothing already collected through it is touched. The documents stay, the invoices stay, and the exports still work. Disconnecting stops future arrivals and nothing else.

POST /v1/profiles/{profileId}/accounts/{id}/sync

Checks this source now instead of waiting for its schedule. ManageSources.

Answers 202, not 200, because the check runs on a background worker and 202 is the honest answer.

Field Type Means
accountId uuid The source that will be checked
queuedAt date-time When the request was accepted. The cooldown runs from here

This is a receipt, not a result. Whether anything was collected shows up where it always does: on the documents list, and on the account's own sync fields.

Refusals:

Status Code Means
404 account-not-found No such source on that client
409 account-sync-not-pullable This source receives rather than fetches. An inbox has nothing to pull
409 account-sync-too-soon Checked too recently. details.availableAt says when it reopens

The cooldown is 15 minutes. Read details.availableAt rather than assuming it.

POST /v1/profiles/{profileId}/accounts/sync

Checks every source on this client that can be checked. ManageSources. Answers 200 with counts rather than 202 with a list.

Field Type Means
queued integer Sources whose check was accepted
skippedTooSoon integer Sources checked too recently to ask again yet
nothingToPull integer Inboxes, manual upload, and disconnected sources waiting on a reconnect

The three add up to every source on the client. A mixed answer is normal and is not an error: the button is "check everything", and refusing the whole request over one member of it would be refusing the only thing that was asked.

{ "queued": 3, "skippedTooSoon": 1, "nothingToPull": 2 }

Supplier connectors

A connector logs into a supplier's own portal and fetches invoices from it. Two shapes exist, and the catalog says which one a given connector uses.

Each connector is switched on per deployment. The catalog lists only the connectors switched on for this deployment, so read it rather than assuming a slug exists. A connector switched on before its credentials were filled in answers connector-not-configured at connect time. In production, new connectors are switched on one at a time as their credentials are set up. Nothing on your side changes that. To ask for one, write to sales@syllevo.com.

GET /v1/connectors

Every connector this host ships, and the inputs each one asks the client for. Answers a plain array.

Field Type Means
slug string Use this in the three routes below
credentialMode ConnectorCredentialMode 0 redirect approval, 1 pasted secret
fields array What to ask for, in a pasted-secret connector

Each field:

Field Type Means
name string The key to send in fields on the credentials route
labelKey string Translation key for the label. Not a label
secret boolean Whether to mask it on screen and keep it out of logs

The redirect flow, credentialMode 0

  1. POST /v1/connectors/{slug}/connect with { "profileId": ..., "directorySlug": ... }. directorySlug is optional and ties the connection to a directory-backed supplier. Answers { "url": "..." }.
  2. Send the person to that URL. The supplier asks them to approve.
  3. The supplier redirects back to your application with a query string. Post it whole: POST /v1/connectors/{slug}/callback with { "state": "...", "query": { ... } }, where query is the callback's query parameters as a JSON object.
  4. Answers 200 with the connected account.

The state value is single use and expires. A callback posted twice, or late, answers 409 connector-state-invalid, and the fix is to start again at step 1.

The pasted-secret flow, credentialMode 1

POST /v1/connectors/{slug}/credentials:

Field Type Required Means
profileId uuid yes The client company
fields object yes Keyed by the name values from the catalog
directorySlug string no Tie it to a directory-backed supplier

The connector proves the credentials work before anything is stored. Credentials that do not work answer 409 connector-credential-invalid and nothing is saved.

Using the wrong flow for a connector answers 409 connector-mode-mismatch.

GET /v1/connectors/accounts

Every supplier account connected for one client. View. Answers a plain array.

Parameter Type Means
profileId uuid The client company
Field Type Means
accountId uuid The connection
profileId uuid The client company
connectorSlug string Which connector
directorySlug string or null The supplier behind it
externalAccountId string The account id at the supplier
externalAccountName string Its name at the supplier
externalFiscalIdentifier string or null The fiscal code at the supplier
credentialState SupplierConnectorCredentialState 0 valid, 1 expired, 2 revoked, 3 refused
credentialExpiresAt date-time, null When it will need renewing
lastSuccessfulPollAt date-time, null Last time it fetched something
nextPollAt date-time When it will try again
lastErrorCode string or null Why the last poll failed

lastErrorCode here describes the last background poll, not your request. See the connector codes in Error codes.

DELETE /v1/connectors/accounts/{accountId}

Tells the supplier Syllevo is done and removes the source. ManageSources. Answers 204. Documents already collected through it stay.

Unfiled mail

Mail that arrived through a source and could not be filed: a portal invitation, a verification code, a "confirm this address" nobody at the firm ever saw. This is the screen that answers why a source has gone quiet.

GET /v1/ingestion/unfiled

One page, newest arrival first. View.

Parameter Type Means
profileId uuid Narrow to one client
accountId uuid Narrow to one source
unresolvedOnly boolean Only what nobody has dealt with yet
page integer Default 1
pageSize integer Default 25, clamped to 200
Field Type Means
id uuid The item
profileId uuid The client company
accountId uuid The source it came through
receivedAt date-time When the mail arrived
createdAt date-time When the row was written
senderAddress string or null Who sent it
senderDisplayName string or null Their display name
subject string or null The subject line
links array Links found in the message. See below
linkCount integer How many were found
linksTruncated boolean True when links holds fewer than linkCount
forwardedAt date-time, null When it was forwarded on
forwardedTo string or null Where it was forwarded
resolvedAt date-time, null Null while it still needs attention
resolvedById uuid or null Who closed it
fieldsPurgedAt date-time, null When the sender, subject, and links were dropped

Each link:

Field Type Means
url string The link as it appeared
host string Its host
clickable boolean Whether it is safe to offer as a link
decision UnfiledLinkDecision Why, see below
fetchFailed boolean Whether Syllevo tried to follow it and could not

decision:

Value Name Means
0 Clickable The sender and the host both check out
1 SenderUnauthenticated The message failed its own authentication
2 SenderNotOnAllowlist The sender is not a supplier Syllevo recognizes
3 HostNotPermittedForSender The link points somewhere that sender should not send to
4 Unreadable The link could not be parsed

Do not make a link clickable when clickable is false. These messages arrive from outside and are exactly where a phishing link would be. The field is the answer, not a suggestion.

fieldsPurgedAt is set when the sender, subject, and links have been dropped on retention. The row stays so the count is honest, but those fields are then null.

POST /v1/ingestion/unfiled/{id}/resolve

Closes one item: the invitation was accepted, the code was used, or the mail was noise. ManageSources on the client the source belongs to. Answers 200 with the item, now carrying resolvedAt and resolvedById.