Sari la conținut
Pagini APIInvoices

Documentație API

Această documentație este disponibilă doar în limba engleză.

Invoices

An invoice is what Syllevo read out of a document: who billed the client, for what, and for how much. A document is the file that arrived. An invoice is the meaning taken from it. The two are separate rows and this page is about the second. For the files, see Documents.

Syllevo does not issue invoices. There is no route that creates, numbers, or changes one, and there will not be one. There is one route that takes in a sales invoice the business already issued in its own software, so the accountant has sales next to purchases. See Import a sales invoice. When a business opts in, Syllevo can also upload that same XML file to SPV, exactly as the invoicing software produced it. See Send a sales invoice to SPV.

Every invoice has a direction. 0 Incoming is a purchase: a supplier billed the client, and this is what Syllevo collects and chases. 1 Outgoing is a sale the client issued and imported. Sales never count towards completeness, reminders, missing-invoice predictions, or suppliers, and the two anomaly detectors skip them. The one flag a sale can carry is ConflictingCopy (see Anomalies), raised when e-Factura's copy of it doesn't match the one imported. When they don't match, e-Factura's copy is the one the sale is read from. A sale imported as a PDF alone isn't flagged when e-Factura's XML of it agrees on currency and amounts: the XML just completes it.

Invoice routes are flat rather than nested under a profile, because an invoice is addressable on its own. A completeness row, a search hit, and a link in a reminder all carry an invoice id and no profile id. The permission still lives on the profile: the API loads the invoice inside the caller's organization first, then asks whether the caller holds View on the client it belongs to.

Routes

Method Path Needs
GET /v1/invoices View
GET /v1/invoices/facets View
GET /v1/invoices/number-gaps View
GET /v1/invoices/{id} View
POST /v1/profiles/{profileId}/sales-invoices ManageDocuments
POST /v1/sales-invoices/{id}/deliveries TransmitInvoices
GET /v1/sales-invoices/{id}/deliveries View
POST /v1/sales-invoices/{id}/submission TransmitInvoices
GET /v1/sales-invoices/{id}/submission View
GET /v1/anomalies View
GET /v1/anomalies/count View
POST /v1/anomalies/{id}/dismiss View

The invoice shape

Field Type Means
id uuid The invoice
profileId uuid The client company it belongs to
direction InvoiceDirection 0 Incoming (a purchase), 1 Outgoing (a sale)
supplierId uuid or null The supplier. Always set on a purchase, always null on a sale. See Suppliers
supplierName string or null The supplier's name at read time, null if it was not known
number string The invoice number as printed
numberSeries string or null The series part of the number, uppercased: FCT for FCT 0042. See number series
numberSequence decimal or null The running number inside the series: 42 for FCT 0042. Null when the number ends in no digits
issueDate date The date on the invoice. This decides which month it is in
dueDate date or null Null when the invoice does not state one
currencyCode string Three letters, RON, EUR, and so on
netAmount decimal Before VAT
vatAmount decimal The VAT
totalAmount decimal What is owed. Negative on a credit note, and so are the net and the VAT
documentTypeCode string or null 380 invoice, 381 credit note, from the XML. Null when there is no XML to read
isCreditNote boolean True for a 381, or when totalAmount is negative. See credit notes
correctedInvoiceNumber string or null On a credit note, the number of the invoice it corrects, as printed
correctedInvoiceIssueDate date or null The issue date of that invoice, when the credit note states one
correctedInvoiceId uuid or null That invoice, when exactly one on file matches. Only on GET /v1/invoices/{id}
documentLinks array The stored files that evidence it, most trusted first
source DataProviderType or null How the most trusted file behind it arrived. Null when every file is in the trash
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
submission object or null On a sale sent to SPV through Syllevo, its latest submission in short. Null otherwise. See below
deliveries array Reserved. Always empty for now. Emailed copies are listed by GET /v1/sales-invoices/{id}/deliveries

submission is the latest SPV submission, in short:

{
  "channel": "efactura",
  "id": "0199a1b2-0000-7000-8000-000000000001",
  "state": 3,
  "uploadIndex": "5001234567",
  "lastErrorCode": null,
  "updatedAt": "2026-09-23T08:15:00Z"
}

state is the submission state and lastErrorCode is one of the submission reasons. deliveries is reserved and always empty: emailed copies are listed by GET /v1/sales-invoices/{id}/deliveries.

Each entry in documentLinks:

Field Type Means
documentId uuid Download it from Documents
reason InvoiceDocumentLinkReason Why this file is attached to this invoice
trustRank InvoiceSourceTrust How much the reading from it can be trusted

reason:

Value Name Means
0 OriginalSource This is the file the invoice was read out of
1 ContentHashMatch Byte for byte the same file, arriving a second time
2 SemanticMatch A different file carrying the same invoice, a PDF beside an XML
3 ManualLink Somebody attached it by hand
4 SupportingDocument Attached as evidence, a receipt or a delivery note
5 SubmissionAnswer ANAF's answer to a sale Syllevo sent to SPV
6 TaxAuthorityVersion e-Factura's copy of a sale, used over a different one on file

A SubmissionAnswer file is proof of what ANAF did with the sale, not a copy of the invoice. It never sets source, so "Came in through" stays how the sale arrived. It is never the file a sale is emailed, matched, or re-imported by, and it is not in the sale's file list in an export, where it travels as a document of its own.

trustRank:

Value Name Means
0 Extracted Read out of a PDF or an image
1 StructuredXml Read out of structured XML
2 EFactura Came from ANAF e-Factura. The highest trust there is

An invoice with several links is normal, and it is not a duplicate. One invoice often arrives twice, once as an e-Factura XML and once as the supplier's own PDF. Syllevo keeps both files and reads the one it trusts most. Take the amounts from the invoice, and pick the file at the top of documentLinks when you need something to show a human.

Credit notes

A credit note (storno) reverses all or part of an earlier invoice. Its amounts are negative here: netAmount, vatAmount, and totalAmount. Add up a client's invoices and the credit notes subtract on their own.

An e-Factura credit note prints positive amounts and says what it is with its type code, 381. Syllevo reads the code and stores the amounts with the minus sign. A credit note that already printed negative amounts is stored as printed, never flipped back to positive.

Use isCreditNote to tell them apart rather than testing documentTypeCode. It is also true for a credit note that only came as a PDF with a printed minus, where there is no code to read.

When the credit note names the invoice it corrects, correctedInvoiceNumber and correctedInvoiceIssueDate say which. GET /v1/invoices/{id} also looks for that invoice in the same client company, the same direction, and for a purchase the same supplier. When exactly one matches, its id is in correctedInvoiceId. When none or several match, it is null.

Invoices filed before 2026-10-12 get their type code from their XML over the following days. Until then documentTypeCode is null on them.

GET /v1/invoices

One page of invoices, newest issue date first unless you ask for another order.

Parameter Type Default Means
profileId uuid none Narrow to one client company
direction incoming or outgoing incoming Purchases or sales. Never both in one page
from date none Earliest issue date, inclusive
to date none Latest issue date, inclusive
series string none Only numbers in this series, case ignored. Up to 64 characters
withoutSeries boolean false true keeps only numbers with no series. Can't be sent with series
sortBy InvoiceSortField 0 0 issue date, 1 number, 2 buyer, 3 total
sortDirection SortDirection 1 0 ascending, 1 descending, for whichever field sortBy names
page integer 1
pageSize integer 25 Clamped to 200

from and to bound the issue date, not the arrival date, and both ends are inclusive. Asking for from=2026-09-01&to=2026-09-30 gives you September, including the 30th. The issue date is what decides which month a document belongs to, which is the same rule an accountant uses.

An invoice can arrive in October and still be a September invoice. If you are reconciling a month that is already closed, poll this route again rather than assuming what you pulled on the 1st is final.

Number series

Most invoicing software numbers sales as a series plus a running number, like FCT 0042 or SYL-2026-118. Syllevo reads both parts off number when the invoice is stored, the same way for every invoice, and returns them as numberSeries and numberSequence:

  1. Spaces at either end go, and letters are uppercased.
  2. A year at the very end goes, when a separator (/, -, or ., spaces allowed around it) stands between it and a digit before it. FCT 12/2026 reads as FCT 12. A year stuck to the series, like SYL2026, stays, because nothing says it's a year.
  3. The last run of digits is numberSequence, read as a number, so leading zeros don't count.
  4. What stands before it, with trailing spaces and -, /, ., _, and # removed, is numberSeries.
number numberSeries numberSequence
FCT 0042 FCT 42
syl-2026-118 SYL-2026 118
FCT 12/2026 FCT 12
000981 null 981
PROFORMA null null

A number with no digits at the end has no series and no sequence. A run of more than 28 digits, leading zeros aside, has no numberSequence, and its series still reads as usual.

series matches numberSeries exactly after spaces at either end are removed and case is ignored, so series=fct finds FCT 0042. Send withoutSeries=true for the numbers that have no series at all. Sending both answers 400.

Order

Every order ends on the invoice id, so a page boundary never moves between two calls.

sortBy Order
0 issue date Issue date
1 number Series, numbers with no series last, then the client, then the year of issue, then numberSequence as a number, so FCT 9 comes before FCT 10. Ties fall back to the number
2 buyer The buyer's name, case ignored, sales with no buyer name last. Then newest issue date first
3 total totalAmount. Then newest issue date first

sortDirection turns the named field around. In the number order it turns the series, the year, the sequence, and the final fallback on the number. The client, the tie breakers on the other orders, and the place of empty values (always last) stay where they are. The year of issue is in the number order because many businesses start their sequence again at 1 every January: without it, FCT 3 from this year would sit between FCT 2 and FCT 4 from last year.

GET /v1/invoices?profileId=22222222-2222-4222-8222-222222222222&from=2026-09-01&to=2026-09-30 HTTP/1.1
Host: api.syllevo.com
X-Api-Key: syl_live_examplekeynotreal000000000000
{
  "items": [
    {
      "id": "33333333-3333-4333-8333-333333333333",
      "profileId": "22222222-2222-4222-8222-222222222222",
      "direction": 0,
      "supplierId": "44444444-4444-4444-8444-444444444444",
      "supplierName": "Example Utilities SA",
      "number": "EU-2026-000120",
      "numberSeries": "EU-2026",
      "numberSequence": 120,
      "issueDate": "2026-09-04",
      "dueDate": "2026-09-24",
      "currencyCode": "RON",
      "netAmount": 1000.0,
      "vatAmount": 190.0,
      "totalAmount": 1190.0,
      "documentLinks": [
        { "documentId": "55555555-5555-4555-8555-555555555555", "reason": 0, "trustRank": 2 }
      ],
      "source": 1,
      "counterpartyName": null,
      "counterpartyTaxId": null,
      "counterpartyCountryCode": null,
      "submission": null,
      "deliveries": []
    }
  ],
  "page": 1,
  "pageSize": 25,
  "totalCount": 1,
  "totalPages": 1
}

GET /v1/invoices/facets

The counts beside the list: how many invoices fall in each issue month, and how many carry each number series. Takes the list's filters, direction, profileId, from, to, series, and withoutSeries, with the same rules and the same 400 for series with withoutSeries. No paging, no order.

Each count leaves out its own filter, so a picker built on it still offers the other choices once one is picked:

  • periods ignores from and to. One entry per month that has invoices, oldest first. period is YYYY-MM.
  • series ignores series and withoutSeries. One entry per client and series, because two clients can both number sales as FCT. Ordered by series, with null (numbers that have no series) last, then by client.

Months and series with no invoices aren't listed.

GET /v1/invoices/facets?direction=outgoing&profileId=22222222-2222-4222-8222-222222222222&from=2026-09-01&to=2026-09-30 HTTP/1.1
Host: api.syllevo.com
X-Api-Key: syl_live_examplekeynotreal000000000000
{
  "periods": [
    { "period": "2026-08", "count": 31 },
    { "period": "2026-09", "count": 27 }
  ],
  "series": [
    { "profileId": "22222222-2222-4222-8222-222222222222", "series": "FCT", "count": 25 },
    { "profileId": "22222222-2222-4222-8222-222222222222", "series": null, "count": 2 }
  ]
}

GET /v1/invoices/number-gaps

Where a client's sales numbering skips. If Syllevo holds FCT 0041 and FCT 0044, the client issued FCT 0042 and FCT 0043 and they aren't in Syllevo yet. Sales only, so there's no direction: a supplier's numbering skips for reasons that have nothing to do with your client.

Takes profileId, from, to, series, and withoutSeries, with the list's rules and the same 400 for series with withoutSeries. A client the caller can't see has no gaps, never a 403.

How a gap is found:

  • Numbers are compared per client, series, and year of issue, because most series restart in January. FCT 0041 in December and FCT 0001 in January is not a gap.
  • The sequence is the number series split: the last run of digits, after a trailing year is dropped.
  • A number on several invoices counts once, so a duplicate is never a gap. The invoice issued first stands for it.
  • Nothing is reported below the first number on file or above the last. Where a series starts and how far it has got aren't things the numbers on file can tell.

from and to pick which gaps are listed, not which sales are read. A gap is listed when the sale on either side of it was issued inside the range, and those sales are found over the whole year. So FCT 0010 on 30 September and FCT 0013 on 2 October shows under September and under October. series and withoutSeries narrow which series are looked at.

At most 200 gaps come back, ordered by series with null (numbers that have no series) last, then by client, year, and the number before the gap. totalGaps and totalMissing count every gap, including the ones past the 200.

Field Type Means
totalGaps integer Every gap the filters reach
totalMissing number Every missing number across those gaps
gaps[].profileId uuid The client
gaps[].series string, null Upper-cased, like numberSeries. null for numbers that have no series
gaps[].year integer The year of issue both sales belong to
gaps[].missingCount number How many numbers are missing between the two sales. Always 1 or more
gaps[].missingNumbers string[] The first missing numbers, at most 5, written like the sale before the gap
gaps[].previous object The sale just below the gap: invoiceId, number, and issueDate
gaps[].next object The sale just above it, same fields

missingNumbers keeps the client's own spelling, case, and zero padding, so you can search the invoicing software for it as printed. A number that can't be written that way without reading as another series is left out, so the list can be shorter than missingCount even for a short gap.

GET /v1/invoices/number-gaps?profileId=22222222-2222-4222-8222-222222222222&from=2026-09-01&to=2026-09-30 HTTP/1.1
Host: api.syllevo.com
X-Api-Key: syl_live_examplekeynotreal000000000000
{
  "totalGaps": 1,
  "totalMissing": 2,
  "gaps": [
    {
      "profileId": "22222222-2222-4222-8222-222222222222",
      "series": "FCT",
      "year": 2026,
      "missingCount": 2,
      "missingNumbers": ["FCT 0042", "FCT 0043"],
      "previous": {
        "invoiceId": "55555555-5555-4555-8555-555555555555",
        "number": "FCT 0041",
        "issueDate": "2026-09-12"
      },
      "next": {
        "invoiceId": "66666666-6666-4666-8666-666666666666",
        "number": "FCT 0044",
        "issueDate": "2026-09-19"
      }
    }
  ]
}

GET /v1/invoices/{id}

One invoice, with documentLinks ordered most trusted first. View on the client it belongs to. This is the only read that fills in correctedInvoiceId. Lists always answer it null.

An invoice the caller cannot see answers 404.

POST /v1/profiles/{profileId}/sales-invoices

Imports one sales invoice the business already issued, for example from its invoicing software. Syllevo does not issue it, number it, or change it. It files it, and sends it to SPV only when you ask with submit=true.

Needs ManageDocuments on the profile and an Idempotency-Key header (see Conventions). Send multipart/form-data:

Part Type Means
xml file The invoice as UBL 2.1 or CIUS-RO XML
pdf file The printable copy
number string The invoice number, up to 64 characters
issueDate date YYYY-MM-DD
dueDate date YYYY-MM-DD, not before the issue date
currency string Three letters
netAmount decimal Before VAT, written 1234.50
vatAmount decimal The VAT
totalAmount decimal What the customer owes
customerName string Who was billed
customerTaxId string Their tax id
customerCountry string Two letters
emailTo string Optional. Email the PDF to these buyers
submit boolean Optional. true sends the XML to SPV

Send xml, pdf, or both. Each file is capped at 25 MB, and its first bytes must match the part it was sent as.

  • With XML, every fact is read from the XML. The other fields are optional. Anything you send anyway must agree with the XML, or the answer is 400 sales_invoice_metadata_mismatch with each field that disagrees in details, next to what the XML says. The XML wins where you send nothing. For a credit note (381), send the amounts as the XML prints them or with the minus sign. Both agree, and Syllevo stores them negative either way. Pick one for the whole request: a printed net beside a negative total is a mismatch.
  • With only a PDF, Syllevo reads nothing out of it, so number, issueDate, currency, netAmount, vatAmount, and totalAmount are required.

The number and issue date identify the sale inside one profile. That decides the answer:

Status When
201 A new sales invoice was filed. Location points at GET /v1/invoices/{id}
200 The same files were already imported under this number and date. duplicate: true
409 invoice_number_exists: a different invoice already has this number and date
409 sales_invoice_file_in_use: one of the files is already a purchase or another sale
400 Validation, sales_invoice_xml_unreadable, or sales_invoice_metadata_mismatch

Numbers match ignoring case and spaces at either end: F-001, f-001, and F-001 are one number. That holds even when two imports arrive at the same moment.

A replay under the same Idempotency-Key answers with the first response, Location header included.

400 sales_invoice_xml_unreadable means one of two things, and nothing is stored in either case:

  • The XML isn't an invoice we can read, or it has no number, issue date, currency, or total.
  • The XML reads fine, but states a value no invoice can hold. For example, a buyer country of ROU instead of RO, or a buyer name longer than 256 characters. errors names each such field, keyed like the form parts (customerCountry, customerName, customerTaxId, number, currency, netAmount, vatAmount, totalAmount), and details.field names the first. Fix the XML in your invoicing software and send it again.

An issued invoice is never overwritten. If 409 invoice_number_exists comes back, details carries existingInvoiceId. Look at that invoice before doing anything else: either you are sending the same sale twice with different files, or two sales carry the same number.

That includes a sale Syllevo already collected from the business's SPV sent list. That copy is the one ANAF holds, so a different file under its number and issue date gets the same 409, and nothing is stored. Your invoicing software and ANAF disagree about that invoice, and the fix belongs in your software. The other order is covered under Anomalies: import first, and a different SPV copy that arrives later becomes the sale's main copy.

One case is finished instead of refused. An import that failed halfway before September 2026 could leave a sale with no files at all, and every retry then got 409. If you send the same files again and the sale has the same currency and total, Syllevo attaches them to that sale and answers 201. The audit trail records it as invoice.import_repaired.

With XML, the customer is read from the XML, from UBL and CII alike. The name, tax id, and country are whatever the buyer block prints.

A customerTaxId you send agrees with the XML's when the digits match, ignoring spaces, punctuation, and case. A two-letter prefix counts only when it can disagree: RO12345678 and DE12345678 differ, and 12345678 agrees with RO12345678 unless the buyer's country is known and isn't RO.

A sale that is in the trash

Trashing a sale moves its files to the trash. The invoice record stays, so its number and date are still taken for the 60 days before the files are deleted for good.

You import again Answer
The same files 200, duplicate: true, restored: true, the existing invoice. The files come back out of the trash, as if restored
Different files, same number 409 invoice_number_exists, as for any other sale with that number
Anything, after the files purge 201. The purge removes a sale that has no files left, which frees its number

Sending the same files again counts as asking for them back. Each file that comes out of the trash this way is recorded in the audit trail as document.restored with reason: "rearrival". To bring a trashed sale back without sending it again, restore its document with POST /v1/documents/{id}/restore. Trashed sales are listed by GET /v1/documents?trashed=true&direction=1.

A sale that is on its way to SPV, or that ANAF accepted, can't go to the trash. DELETE on any of its files, ANAF's answer archive included, answers 409 sales_invoice_in_spv. Once a submission ends Rejected, Unreadable, or Failed, the sale can be trashed, and the answer archive goes with it. The 60-day purge never deletes the XML or the answer archive of a submission that is still in flight or was accepted.

POST /v1/profiles/22222222-2222-4222-8222-222222222222/sales-invoices HTTP/1.1
Host: api.syllevo.com
X-Api-Key: syl_live_examplekeynotreal000000000000
Idempotency-Key: postify-sale-2026-000481
Content-Type: multipart/form-data; boundary=----b

------b
Content-Disposition: form-data; name="xml"; filename="F-2026-000481.xml"
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">...</Invoice>
------b
Content-Disposition: form-data; name="pdf"; filename="F-2026-000481.pdf"
Content-Type: application/pdf

%PDF-1.7 ...
------b--
{
  "invoice": {
    "id": "66666666-6666-4666-8666-666666666666",
    "profileId": "22222222-2222-4222-8222-222222222222",
    "direction": 1,
    "supplierId": null,
    "supplierName": null,
    "number": "F-2026-000481",
    "issueDate": "2026-09-18",
    "dueDate": "2026-10-18",
    "currencyCode": "RON",
    "netAmount": 500.0,
    "vatAmount": 95.0,
    "totalAmount": 595.0,
    "documentLinks": [
      { "documentId": "77777777-7777-4777-8777-777777777777", "reason": 0, "trustRank": 1 },
      { "documentId": "99999999-9999-4999-8999-999999999999", "reason": 4, "trustRank": 0 }
    ],
    "counterpartyName": "Example Customer SRL",
    "counterpartyTaxId": "RO12345678",
    "counterpartyCountryCode": "RO",
    "submission": null,
    "deliveries": []
  },
  "duplicate": false,
  "restored": false,
  "deliveries": []
}
Field Type Means
invoice object The sales invoice, in the invoice shape
duplicate boolean true when the same files were already imported under this number and date
restored boolean true when this repeat brought at least one of the invoice's files out of the trash
deliveries array The emails asked for with emailTo. Empty when you didn't send it
submission object or null The SPV submission asked for with submit=true, new or existing. Null otherwise
submissionError object or null Why nothing was queued for SPV, in the error shape. Null otherwise

restored is true only on a repeat (duplicate: true) that found one or more of the invoice's files in the trash and brought them back. A plain repeat with nothing in the trash, and a new import, answer false. Retrying with the same Idempotency-Key returns the first answer unchanged.

Emailing it on import

Leave emailTo out and nothing is emailed. Send it, repeated or as one comma-separated value, with 1 to 10 addresses, and Syllevo emails the PDF to each of them once the invoice is filed. This works exactly like emailing a sales invoice with resend false, and needs TransmitInvoices on the profile as well as ManageDocuments.

  • Both permissions are checked before anything is filed, so a caller who may import but not email gets 403 and nothing is imported.
  • emailTo without a pdf part answers 422 sales_invoice_pdf_missing, also before filing.
  • The answer carries a top-level deliveries array next to invoice and duplicate, one entry per address, in the delivery shape. Repeating an import that was already filed returns the existing deliveries and emails nobody a second time.

Sending it to SPV on import

Leave submit out and nothing is sent. Send submit=true and, once the invoice is filed, Syllevo queues its XML for SPV exactly like POST /v1/sales-invoices/{id}/submission.

  • submit=true without an xml part answers 422 spv_xml_missing before anything is filed.
  • Every other refusal leaves the import standing. The answer is still 201 or 200, with submission: null and the reason in submissionError, for example { "status": 403, "code": "spv_consent_missing", "messageKey": "web.errors.spv_consent_missing" }. Fix the cause and call POST /v1/sales-invoices/{id}/submission.
  • A repeat of an import that was already sent returns the existing submission in submission and sends nothing twice.

The files are stored as documents like any other and can be downloaded from Documents by id. They do not show up in the documents list, the review queue, or the trash, which are about purchases.

Email a sales invoice to its buyer

Syllevo emails a sales invoice only when you ask, on the call in front of it. Your invoicing software probably emails your buyers already, so Syllevo never does it on its own: no default, no setting, and no automatic send after an import. Each address named on a call gets one email with the invoice's PDF attached.

POST /v1/sales-invoices/{id}/deliveries

Needs TransmitInvoices on the profile the sale belongs to and an Idempotency-Key header (see Conventions). TransmitInvoices is off for members by default, because sending an invoice to a buyer speaks for the business. SendReminders does not cover it: that permission is about mail to the firm's own client.

{ "to": ["billing@example-customer.ro"], "resend": false }
Field Type Means
to string[] 1 to 10 addresses. Case and spaces are ignored, and repeats count once
resend boolean Optional, default false. true emails addresses that already got one

What each address gets:

The address already has resend: false resend: true
Nothing, or only failed deliveries A new delivery A new delivery
A delivery still Queued That delivery, no second email That delivery, no second email
A delivery that was Sent That delivery, no second email A new delivery, resendOfId set to it

The answer is 202 Accepted: a background worker sends the email, usually within seconds. The body is { "deliveries": [...] }, one entry per address in the order you sent them.

Status When
202 Every address has a delivery, new or existing
400 Validation: no addresses, more than 10, or one is not an email address
403 The caller can see the invoice but lacks TransmitInvoices
404 No sales invoice with this id that the caller can see. A purchase answers 404 too
422 sales_invoice_pdf_missing: the sale was imported without a PDF, so there is nothing to send

GET /v1/sales-invoices/{id}/deliveries

Every delivery the sale has, oldest first, as { "deliveries": [...] }. Needs View.

The delivery shape

Field Type Means
id uuid The delivery
invoiceId uuid The sale
to string The address, lowercased
state integer 0 Queued, 1 Sent, 2 Bounced (reserved), 3 Failed
attempts integer How many times the worker tried to send it
lastError string or null Why the last attempt failed. See below
resendOfId uuid or null The sent delivery this one repeats, when it is a resend
requestedByUserId uuid or null Who asked, when it was a person
requestedByApiKeyId uuid or null Which API key asked, when it was an integration
createdAt datetime When it was asked for
lastAttemptAt datetime or null When the worker last tried
sentAt datetime or null When our mail relay accepted it

lastError codes:

Code Means Final
recipient_rejected The receiving mail server refused the address Yes
pdf_missing The PDF was deleted after the request Yes
invoice_missing The sale was deleted after the request Yes
queue_unavailable Syllevo could not queue the email. The call itself failed with a 5xx Yes
send_failed Our mail relay failed. The worker retries up to five times in all Only after the fifth try
stalled Still queued an hour after its last try. The next request for the address replaces it Yes

While the worker is still retrying, lastError is send_failed and state is still 0 Queued. If the fifth try fails too, state becomes 3 Failed. A failed delivery never blocks a new one: ask again and Syllevo queues a fresh one. A delivery stuck in 0 Queued for more than an hour with no attempt doesn't block one either: the next request marks it 3 Failed with stalled and queues a fresh one. If the worker picks up that delivery at the same moment, the request answers with it instead, so the buyer never gets the email from two deliveries at once.

The email

The sender name is the business's company name, and replies go to the business's primary contact. The address itself is on Syllevo's sending domain, as it is for reminders, so the mail passes SPF and DKIM checks. The subject and text are short, in the business's language, and the PDF is attached under the file name it was imported with.

What Syllevo does not do yet

  • Bounces are not tracked. Sent means our mail relay accepted the email, not that it reached the inbox. A mailbox that bounces it later still shows Sent. The Bounced state is reserved for when Syllevo reads bounce reports.
  • A rare double send. If the worker stops after the relay accepted the email but before it saved Sent, the retry sends it again. The buyer gets two copies, never none.
  • No custom text. The subject and message are fixed. Anything the buyer needs to know belongs on the invoice itself.

Send a sales invoice to SPV

Syllevo sends only the XML the business's invoicing software made, unchanged, and only when you ask. It never writes an invoice, never fixes one, and never sends one on its own. This is for businesses whose invoicing software doesn't upload to SPV itself.

Two switches have to be on first, and both are off by default:

  1. The firm. Syllevo turns sales invoice transmission on per organization. There is no setting for it in the app yet. Until it is on, every request answers 403 spv_transmission_disabled.
  2. The business. The person who connected the business to e-Factura turns on "Allow Syllevo to send sales invoices to SPV" (see e-Factura and SPV). Uploads go out under that person's ANAF certificate, so nobody else can turn it on. Until it is on, requests answer 403 spv_consent_missing.

POST /v1/sales-invoices/{id}/submission

Needs TransmitInvoices on the profile and an Idempotency-Key header. No body.

Before anything is queued, Syllevo checks, in this order:

Check Refused with
Nothing is in flight or accepted for this invoice 409 spv_submission_exists, with that submission
Transmission is on for the firm 403 spv_transmission_disabled
The business opted in 403 spv_consent_missing
The invoice isn't already in SPV (not on the business's sent list) 409 spv_already_in_spv
The invoice has an XML file, still in storage 422 spv_xml_missing
The XML is a UBL Invoice or CreditNote in the CIUS-RO format 422 spv_xml_invalid, reason in details.reason
The seller's CUI in the XML is the business's CUI 422 spv_seller_mismatch
The business is connected to e-Factura for that CUI 422 spv_not_enrolled
ANAF lists the connected certificate's rights for that CUI as active 422 spv_cui_not_authorized

details.reason on spv_xml_invalid is not_xml, not_ubl, not_cius_ro, or unreadable.

The answer is 202 Accepted with the new submission, in the submission shape. A background worker runs the same checks again, asks ANAF's validator, and uploads. It never uploads a file the validator hasn't passed: if the validator can't be reached, the submission waits and tries again later.

The 409 spv_submission_exists body is the usual error shape plus the existing submission:

{
  "status": 409,
  "code": "spv_submission_exists",
  "messageKey": "web.errors.spv_submission_exists",
  "submission": { "id": "0199a1b2-0000-7000-8000-000000000001", "state": 3 }
}

At most one submission per invoice is in flight at a time, and at most one is ever accepted. After a final state other than Accepted, you can ask again, and a new submission starts.

GET /v1/sales-invoices/{id}/submission

Needs View. Answers { "latest": {...} or null, "history": [...] }, newest first. latest is null when the invoice was never sent through Syllevo.

The submission shape

Field Type Means
id uuid The submission
invoiceId uuid The sale
profileId uuid The business
channel string efactura
state integer See submission states
cui string The seller's CUI it was sent under, digits only
uploadIndex string or null ANAF's index_incarcare, once ANAF gave one
downloadId string or null The id of ANAF's answer, once there is one
attempts integer How many uploads were started
lastErrorCode string or null Why it stopped or is waiting. See below
lastErrorMessage string or null ANAF's own words, often in Romanian, when ANAF gave any
xmlDocumentId uuid The XML file that was sent
responseArchiveDocumentId uuid or null ANAF's answer archive, stored as a document on the invoice
requestedByUserId uuid or null Who asked, when it was a person
requestedByApiKeyId uuid or null Which API key asked, when it was an integration
requestedByName string or null The person's name (or email), or the API key's name. See below
createdAt datetime When it was asked for
updatedAt datetime Its last change
uploadedAt datetime or null When ANAF gave it an upload index
completedAt datetime or null When it reached a final state
uncertainSince datetime or null When Syllevo stopped knowing whether the upload reached ANAF

Syllevo fills in requestedByName, so a member who can't list the team or the API keys still sees who sent the sale. When requestedByApiKeyId is set it holds the key's name alone, for the app to label as an API key in the reader's language. It is null when the person has left the firm.

Submission states

Value State Means Final
0 Queued Waiting for the worker, or for a retry after ANAF was unreachable No
1 Validating The checks and ANAF's validator are running No
2 Sending The upload is going out No
3 Uploaded ANAF gave it an index and is processing it. Syllevo checks back No
4 Accepted ANAF accepted it Yes
5 Rejected ANAF processed it and rejected it. lastErrorMessage says why Yes
6 Unreadable ANAF refused the file or couldn't read it Yes
7 Uncertain The upload may or may not have reached ANAF. See below No
8 Failed Nothing reached ANAF. lastErrorCode says why Yes

Uncertain is the safety net. If the connection drops mid-upload, Syllevo can't know whether ANAF got the file, and sending it again could file it twice. So it never re-uploads an Uncertain submission on its own. It watches the business's SPV sent list instead, and when the exact file it uploaded shows up there (the same hash) or an entry carries that upload's index, the submission moves on with the index and answer found there. A sent-list entry with the same number and date but a different file never settles it. If nothing has shown up after 3 days, the firm's Owners get one email asking them to check SPV by hand.

Syllevo retries only when the error proves the request never reached ANAF: it couldn't resolve or connect to ANAF's host, the TLS handshake failed, ANAF's gateway refused the token (401 or 403), or ANAF said the rate limit was hit. Those go back to Queued with a growing wait (2, 5, 15, 30, then 60 minutes), for up to 6 tries. A validator that can't be reached goes back to Queued the same way, with anaf_unreachable. Nothing was uploaded, so that wait has no try limit, only the 3-day one below.

A submission still Queued 3 days after the request stops as Failed with send_failed.

If a submission looked stuck and was marked Uncertain while its upload was in fact still going, ANAF's answer wins when it arrives: an upload index moves it to Uploaded, and a refusal ends it. The audit trail marks those as settled by a race.

While a submission is Uploaded, Syllevo asks ANAF for its status after 1 minute, then 2, 4, 8, 16, and 32 minutes, then hourly. When it is final, Syllevo downloads ANAF's answer archive once and stores it on the invoice as a document.

The deadline alert. Romanian law gives an invoice 5 working days from its issue date to reach SPV. If a sale sent through Syllevo isn't Accepted after 3 working days (Romanian public holidays excluded), the firm's Owners get one email about it.

Why a submission stopped

lastErrorCode is one of the spv_* codes in the checks table above (the worker runs the checks again right before sending), or one of these:

Code Means
validation_failed ANAF's validator found errors. lastErrorMessage lists them. Failed
upload_refused ANAF refused the file without giving it an index. Unreadable
anaf_unreachable ANAF or its validator couldn't be reached. Still Queued, retrying
send_failed ANAF stayed unreachable through every try. Failed
outcome_unknown The upload may have reached ANAF. Uncertain
rejected ANAF rejected it. lastErrorMessage says why. Rejected
not_ingested ANAF gave it an index, then couldn't process it. Unreadable
status_unavailable The last status check got no answer. Still Uploaded, checking again
xml_changed The invoice's XML file changed after the request. Failed
invoice_gone The invoice was deleted before it was sent. Failed
interrupted The worker stopped before the upload went out. Back to Queued
token_unavailable The e-Factura connection had no usable token. Back to Queued

Anomalies

Syllevo runs two conservative detectors over collected invoices and raises a flag when one fires. A flag is a suggestion to look, not a refusal and not a correction. Nothing is held back because of one.

Value Kind The claim
0 AmountOutlier This total is far above what this supplier usually bills
1 PossibleDuplicate This may be the same invoice already on file
2 ConflictingCopy Another source sent a copy of this invoice with different content

Both detectors are tuned to stay quiet. If you are building a review queue, an open flag is worth a human's attention.

ConflictingCopy is not a detector's guess. Today it's raised on a sales invoice imported through the API when e-Factura's sent list carries the same number and issue date with a different file (e-Factura and SPV). e-Factura's copy wins: it's the version ANAF holds, so it's the one the accountant books. It's linked to the sale with trustRank 2 and reason 6 (TaxAuthorityVersion), which puts it at the top of documentLinks, and the sale's number, dates, amounts, and buyer are read again from it. The imported file stays linked beside it, and counterpartDocumentId is that imported file, so the two can be compared. The audit trail entry lists which fields changed. supplierName is null on this kind, because a sale has no supplier.

A sale imported as a PDF alone is the common case, and it isn't a conflict when the two agree. If the sale has no XML file and e-Factura's XML carries the same currency, net, VAT, and total, the XML is linked with trustRank 2 and reason 2 (SemanticMatch), becomes the main copy, fills in anything the PDF import left blank or wrote differently (the buyer, the due date), and no flag is raised. The audit trail records it as invoice.spv_sent_linked with resolution completed_pdf_import, naming both files and the fields that changed. If the currency or any amount differs, it's a conflict like any other. Two different XML files are always a conflict, even when they carry the same amounts.

One exception: if the sale's main copy is already one ANAF holds (it came from the sent list, or Syllevo sent it to SPV and the sent list returned the same bytes), a second, different e-Factura copy doesn't replace it. That copy is stored as its own document, linked to no invoice, and counterpartDocumentId names it instead.

GET /v1/anomalies

One page of flags, newest first. Open flags only unless you ask for the settled ones.

Parameter Type Default Means
profileId uuid none Narrow to one client company
kind InvoiceAnomalyKind none 0 amount outlier, 1 possible duplicate, 2 conflicting copy
includeDismissed boolean false Add the flags somebody already settled
page integer 1
pageSize integer 25 Clamped to 200
Field Type Means
id uuid The flag
kind InvoiceAnomalyKind Which detector raised it
invoiceId uuid The invoice it is about
invoiceNumber string Its number, so you do not have to fetch it
profileId uuid The client company
supplierName string or null The supplier
issueDate date The invoice's issue date
totalAmount decimal The invoice's total
currencyCode string Its currency
medianTotalAmount decimal or null The usual total from this supplier. Amount outliers only
amountRatio decimal or null How many times the usual total this one is. Outliers only
counterpartInvoiceId uuid or null The invoice it may duplicate. Duplicates only
counterpartDocumentId uuid or null The copy that isn't used. Conflicting copies only
createdAt date-time When the flag was raised
dismissedAt date-time, null Null while the flag is open
dismissedById uuid or null Who settled it
{
  "items": [
    {
      "id": "88888888-8888-4888-8888-888888888888",
      "kind": 0,
      "invoiceId": "33333333-3333-4333-8333-333333333333",
      "invoiceNumber": "EU-2026-000120",
      "profileId": "22222222-2222-4222-8222-222222222222",
      "supplierName": "Example Utilities SA",
      "issueDate": "2026-09-04",
      "totalAmount": 1190.0,
      "currencyCode": "RON",
      "medianTotalAmount": 200.0,
      "amountRatio": 5.95,
      "counterpartInvoiceId": null,
      "counterpartDocumentId": null,
      "createdAt": "2026-09-05T04:10:00Z",
      "dismissedAt": null,
      "dismissedById": null
    }
  ],
  "page": 1,
  "pageSize": 25,
  "totalCount": 1,
  "totalPages": 1
}

GET /v1/anomalies/count

How many flags still need a look, in total and per client, over the clients this caller can see. This is the badge number.

Field Type Means
total integer Open flags across every visible client
profiles array { "profileId": uuid, "openCount": integer }

Clients with no open flags are not in the array.

POST /v1/anomalies/{id}/dismiss

Marks one flag handled. View on the client it belongs to.

This is final. The flag never comes back, and detection never raises the same question about the same invoice again. There is no undo route.

Answers 200 with the flag, now carrying dismissedAt and dismissedById.