Skip to content
API pagesDocuments

API documentation

Documents

A document is a file that arrived for a client company, whatever route it came by: a forwarded email, an e-Factura message, a supplier portal, an upload link, or somebody dropping a scan in by hand. The reading taken from it is an invoice, which lives on Invoices.

Syllevo never loses a document. The original is written to immutable storage before any row exists, and it is kept byte for byte as it arrived. Nothing on this page rewrites a file.

Document routes are flat rather than nested under a profile, because a document is addressable on its own. The permission still lives on the client company: the API loads the document inside the caller's organization first, then asks whether the caller holds the right permission on the client it turns out to belong to.

Routes

Method Path Needs
GET /v1/documents View
GET /v1/documents/facets View
GET /v1/documents/{id} View
GET /v1/documents/{id}/download View
POST /v1/documents ManageDocuments
DELETE /v1/documents/{id} ManageDocuments
POST /v1/documents/{id}/restore ManageDocuments
GET /v1/review View
GET /v1/review/count View
GET /v1/review/{documentId} View
POST /v1/review/{documentId}/decision ManageDocuments

The document shape

Field Type Means
id uuid The document
profileId uuid The client company it belongs to
status DocumentStatus See below
originalFileName string or null The name it arrived with
contentType string The verified type, not the declared one
contentHash string Hash of the bytes. Equal hashes mean the same file
sizeBytes integer Size of the stored original
receivedAt date-time The first time these bytes arrived
lastArrivalAt date-time The most recent time they arrived. Sorting default
createdAt date-time When the row was written
lastUpdatedAt date-time, null When it last changed
sourceProviderType DataProviderType Which kind of source it came from
direction InvoiceDirection 0 a purchase the client received, 1 a sale it issued
sourceAccountTag string or null The label on that source, when it has one
lastErrorCode string or null Why processing last failed. See Error codes
invoice object or null A summary of the reading, null while there is none
deletedAt date-time, null Null unless the document is in the trash
deletedByUserId uuid or null Who trashed it
deletedByName string or null Their name
purgeAt date-time, null When a trashed document is deleted for good

status:

Value Name Means
0 Received Stored. Nothing has been read out of it yet
1 Processing Being read now
2 NeedsReview Read, but a person has to settle it. See the review queue below
3 Processed Read and filed. The invoice is on invoice
4 Failed Reading failed. lastErrorCode says why

sourceProviderType:

Value Name Means
0 EmailInbox A forwarded or watched mailbox
1 EFacturaAnaf ANAF e-Factura
2 SupplierPortal A supplier connector
3 CloudStorage A connected drive
4 ManualUpload Uploaded by hand or through a link
5 Api Sent through the public API

invoice, when there is one:

Field Type Means
invoiceId uuid or null The invoice. Fetch it from Invoices
number string or null The invoice number
issueDate date or null The date on the invoice
dueDate date or null When it is payable
totalAmount decimal or null What is owed
currencyCode string or null Three letters
isCreditNote boolean A credit note: type code 381, or a negative total
direction InvoiceDirection 0 a purchase, 1 a sale
supplierId uuid or null The supplier. Null on a sale
supplierName string or null Its name. Null on a sale
supplierDirectorySlug string or null Set when the supplier matched a directory entry
counterpartyName string or null On a sale, the customer's name. Null on a purchase
counterpartyTaxId string or null On a sale, the customer's tax id as printed
counterpartyCountryCode string or null On a sale, the customer's country, two letters
confirmed boolean Whether a person has settled the reading

A sale's document used to answer invoice: null, because the summary looked for a supplier and a sale has none. It now answers the sale, with the customer in the counterparty fields. A reading that has not been settled yet is always a purchase: sales are filed on import and never wait for review.

Use isCreditNote to mark a credit note rather than testing for a negative totalAmount. A 381 with a zero total is still a credit note. On a reading that has not been settled yet, it comes from the type code and total the reading found.

GET /v1/documents

One page of documents, newest arrival first by default.

Parameter Type Default Means
profileId uuid none Narrow to one client company
status DocumentStatus none Narrow to one status
period string none YYYY-MM. The month the document arrived in
supplierId uuid none Keep only documents evidencing that supplier
source DocumentSourceKind none 0 e-Factura, 1 everything else
from date none Earliest arrival day, inclusive
to date none Latest arrival day, inclusive
sortBy DocumentSortField 7 See below
sortDirection SortDirection 1 descending 0 ascending, 1 descending
trashed boolean false Show the trash instead of the working set
direction InvoiceDirection 0 purchases Which trash to show. Only with trashed=true
page integer 1
pageSize integer 25 Clamped to 200

sortBy:

Value Sorts by
0 Received at
1 Issue date on the invoice
2 File name
3 Supplier
4 Status
5 Client company
6 Source
7 Last arrival. The default

Two date rules are worth stating, because they are the opposite of each other and both are right:

  • Documents filter on arrival. period, from, and to here are about when the file came in, because "what came in for October" is what month-end asks of a collected original.
  • Invoices filter on issue date. See Invoices.

Sending period and a from/to range together gives you the overlap of the two, not the union.

Filtering never widens what you can see. Without profileId you get every client this credential can see. With one, that same set is narrowed to one client, so naming a client you cannot see gives an empty page rather than a 403. Asking what is waiting is not a forbidden question just because the answer is nothing.

Sorting orders the whole filtered set before paging, so changing the sort moves rows between pages rather than reordering the page you already have.

GET /v1/documents/facets

How many documents sit behind each choice on the filters. Takes exactly the same parameters as GET /v1/documents.

Field Type Means
statuses array { "status": DocumentStatus, "count": integer }
periods array { "period": "YYYY-MM", "count": integer }
sources array { "source": DocumentSourceKind, "count": integer }
trashed integer How many are in the trash. direction=1 counts sales
earliestPeriod string YYYY-MM of the oldest document, for a month picker

GET /v1/documents/{id}

One document, plus provenance: every arrival it has had, newest first. View.

A document that arrives twice is one row with two provenance entries, not two rows. This is how you answer "where did this come from, and has it come before".

Each provenance entry:

Field Type Means
receivedAt date-time When this arrival happened
providerType DataProviderType How it arrived
accountId uuid or null The source it came through
accountTag string or null That source's label
originalFileName string or null The name on this arrival
senderAddress string or null Who sent it, for email arrivals
sourceMessageId string or null The message id, for email arrivals
externalDocumentId string or null The id at the source, for portals and ANAF
externalTransactionId string or null The transaction id at the source
spfVerdict EmailAuthenticationResult SPF result on the message
dkimVerdict EmailAuthenticationResult DKIM result
dmarcVerdict EmailAuthenticationResult DMARC result

The three verdicts use EmailAuthenticationResult: 0 unknown, 1 pass, 2 fail, 3 soft fail, 4 neutral, 5 none, 6 temporary error, 7 permanent error.

GET /v1/documents/{id}/download

A short-lived link to the stored original. View.

Field Type Means
documentId uuid The document
url uri Fetch the bytes from here. No credential needed
contentType string The verified type
fileName string or null A sensible name to save it as
expiresAt date-time After this the link is dead

Call this each time you need the file. Do not store the URL: it expires, and a stored one is a credential you did not mean to keep.

If the row exists but the stored file behind it does not, this answers 409 with document_original_missing. That is not retryable. Re-signing points at the same absent key.

POST /v1/documents

Drops a document in by hand: the invoice a client handed over on paper, or one that arrived outside every connected source. ManageDocuments.

multipart/form-data with two parts:

Part Means
profileId The client company it is for
file The file itself

Limits:

Limit Value
File 25 MB
Whole request 26 MB
File name 260 characters
Declared type 128 characters

The declared type is checked against the file's own leading bytes before anything is stored. A PDF named .jpg is filed as a PDF. Something claiming to be a PDF and not being one is refused.

POST /v1/documents HTTP/1.1
Host: api.syllevo.com
X-Api-Key: syl_live_examplekeynotreal000000000000
Content-Type: multipart/form-data; boundary=----example

------example
Content-Disposition: form-data; name="profileId"

22222222-2222-4222-8222-222222222222
------example
Content-Disposition: form-data; name="file"; filename="invoice.pdf"
Content-Type: application/pdf

<bytes>
------example--
{
  "duplicate": false,
  "document": {
    "id": "55555555-5555-4555-8555-555555555555",
    "profileId": "22222222-2222-4222-8222-222222222222",
    "status": 0,
    "originalFileName": "invoice.pdf",
    "contentType": "application/pdf",
    "contentHash": "9f2c0000000000000000000000000000000000000000000000000000000000ab",
    "sizeBytes": 84213,
    "receivedAt": "2026-09-11T08:00:00Z",
    "lastArrivalAt": "2026-09-11T08:00:00Z",
    "createdAt": "2026-09-11T08:00:00Z",
    "lastUpdatedAt": null,
    "sourceProviderType": 4,
    "sourceAccountTag": null,
    "lastErrorCode": null,
    "invoice": null,
    "deletedAt": null,
    "deletedByUserId": null,
    "deletedByName": null,
    "purgeAt": null,
    "provenance": []
  }
}

201 means the document is new. 200 means these exact bytes were already on file for this client. In the second case the upload recorded another arrival on the existing document and created nothing, and duplicate is true. That is a success, not an error. Uploading the same file twice is safe, so a client that is not sure whether a send went through can just send it again.

DELETE and POST /v1/documents/{id}/restore

DELETE moves a document to the trash. ManageDocuments. Answers 204 with no body.

Nothing is destroyed at that moment. The document gets deletedAt, deletedByUserId, deletedByName, and a purgeAt about 60 days out, and it disappears from the default list. Pass trashed=true on GET /v1/documents to find it.

POST /v1/documents/{id}/restore brings it back out, clearing those fields. ManageDocuments. Answers 200 with the document.

Sales in the trash

Purchases and sales have separate trash views. GET /v1/documents?trashed=true shows purchases, the same as it always has. Add direction=1 to see trashed sales instead, and send the same parameter to GET /v1/documents/facets to get the sales count in trashed. The other facet counts stay about purchases.

direction=1 without trashed=true is a 400: the working list is the purchases inbox, and it refuses a question about sales rather than answering it with purchases. Imported sales are listed by GET /v1/invoices?direction=outgoing.

DELETE and restore work on a sale's document the same way. Trashing or restoring a sale never changes a completeness count and never touches the usage meter: completeness asks what the client received, and a sale was never counted in it.

The invoice record stays while its files sit in the trash. What happens when the same sale is imported again is in Invoices.

A sale that is on its way to SPV, or that ANAF accepted, stays out of the trash: DELETE on any of its files answers 409 sales_invoice_in_spv. A sale ANAF rejected, or that couldn't be sent, can be trashed as usual, and ANAF's answer archive goes with it.

After purgeAt the original is deleted for good, and there is no route that undoes that.

The review queue

Some documents cannot be filed without a person. The reading might be incomplete, the supplier unidentifiable, or the invoice might look like one already on file. Those get status 2, NeedsReview, and land in this queue.

GET /v1/review

One page of the queue, newest arrival first. View.

Parameter Type Default Means
profileId uuid none Narrow to one client company
page integer 1
pageSize integer 25 Clamped to 200
Field Type Means
documentId uuid The document waiting
profileId uuid The client company
profileName string Its name, so the queue reads without a second call
originalFileName string or null The file name
receivedAt date-time First arrival
lastArrivalAt date-time Most recent arrival
hasCandidate boolean Whether the pipeline has a proposed reading
preFillSuggested boolean Whether that proposal is good enough to pre-fill a form

GET /v1/review/count

The badge: how much is waiting, in total and per client, over the clients this caller can see.

Field Type Means
total integer Documents waiting across every visible client
profiles array { "profileId": uuid, "pendingCount": integer }

GET /v1/review/{documentId}

Everything one review screen needs: the document's facts, and the values the pipeline proposes. View.

Field Type Means
documentId, profileId, profileName Which document and whose
status DocumentStatus Should be 2 while it is waiting
originalFileName, contentType, contentHash, sizeBytes The file
receivedAt, lastArrivalAt date-time Arrivals
hasCandidate boolean Whether there is a proposed reading
preFillSuggested boolean Whether to pre-fill the form with it
values object or null The proposed reading, as a JSON object
fieldScores object or null Per-field confidence, as a JSON object

values and fieldScores are open JSON objects rather than a fixed schema, because the fields a reading proposes depend on what kind of document arrived. Read them defensively.

Use GET /v1/documents/{id}/download to put the original in front of the person deciding.

POST /v1/review/{documentId}/decision

Records a person's answer about one document and applies it. ManageDocuments.

Field Type Required Means
outcome ReviewOutcome yes 0 confirmed, 1 corrected, 2 rejected
correctedFields object only for 1 The corrected values, as a JSON object
notes string no Free text, kept on the record

correctedFields is a JSON object, not a string, and it is a full snapshot of the corrected values rather than a diff. Send every field, including the ones you did not change.

POST /v1/review/55555555-5555-4555-8555-555555555555/decision HTTP/1.1
Host: api.syllevo.com
X-Api-Key: syl_live_examplekeynotreal000000000000
Content-Type: application/json

{
  "outcome": 1,
  "correctedFields": {
    "number": "EU-2026-000120",
    "issueDate": "2026-09-04",
    "totalAmount": 1190.0,
    "currencyCode": "RON",
    "supplierName": "Example Utilities SA",
    "supplierTaxIdentifier": "RO00000000"
  },
  "notes": "Number was misread as EU-2026-00012O."
}
{
  "decisionId": "99999999-9999-4999-8999-999999999999",
  "documentId": "55555555-5555-4555-8555-555555555555",
  "profileId": "22222222-2222-4222-8222-222222222222",
  "outcome": 1,
  "status": 3,
  "decidedAt": "2026-09-11T08:05:00Z"
}

Statuses to expect:

Status Code Means
400 validation_failed correctedFields missing on a correction, or sent on the other two
404 not_found No such document for this caller
409 conflict The document is no longer waiting. A colleague settled it
409 review_possible_duplicate Confirming would file a second copy of an invoice on file
409 review_incomplete_reading The reading is missing fields a confirmation needs
409 review_unidentifiable_supplier The supplier could not be identified. Correct it and resend

The plain 409 is the ordinary two-people-one-queue case rather than an error. Refresh the queue, do not retry.

Decisions are append-only. A reviewer who changes their mind decides again, and both answers stay in the record. That is the point of the record.