Invoice Drafts
A draft is an invoice held in GoRoute that has not been sent. You create it, read it, change it and validate it as often as you like, and nothing leaves GoRoute until you explicitly send it. Drafts are what you build an approval step out of: a document can sit in front of a human for as long as that takes, and the Peppol network never sees it.
Every draft belongs to one organisation. A draft you did not create, in an organisation you are
not authenticated against, does not exist as far as the API is concerned โ the read answers 404,
not 403.
Nothing in GoRoute puts an invoice into an approval queue by itself, and no send path consults your approval rules. If you need a sign-off before a document goes out, hold it as a draft and send it after the sign-off. See an approval step you can build today.
Reading a draftโ
GET /api/v1/drafts/{draft_id}
Returns the draft's stored invoice data together with the validation state recorded the last time it was validated:
| Field | What it is |
|---|---|
id | The draft's identifier |
name | The name you gave the draft, if any |
invoice_number | Extracted from the invoice data for display |
status | Where the draft is in its life โ see below |
invoice_data | The whole invoice, in the same shape you would send to POST /api/v1/invoices |
is_valid | The result of the last validation, not a fresh one |
validation_errors | The errors from that same last validation |
validation_score | A completeness score from 0 to 100, from that same validation |
transaction_id | The transaction the draft became, once it has been sent |
error_message | Why the last send or import failed, when one did |
buyer_name, buyer_peppol_id, currency, total_amount, issue_date, due_date | Extracted from the invoice data for lists and search |
created_at, updated_at | Timestamps |
is_valid, validation_errors and validation_score are stored, not computed on read. They
tell you what validation said when it last ran. If you have changed the draft since, they are
stale โ validate again before you trust them.
A draft that is not yours, or an identifier that does not exist, is refused with
404 "Draft not found". Both reads are scoped to the organisation your credential belongs to.
Validating a draft โ this changes the draftโ
POST /api/v1/drafts/{draft_id}/validate
The answer is the validation result:
{
"is_valid": false,
"score": 85,
"errors": [
{
"code": "BUS_014",
"message": "Net amount is required in totals",
"field": "totals.net_amount"
}
],
"warnings": []
}
Each entry in errors and warnings carries a code, a message and the field it is about.
score is a completeness score from 0 to 100. It starts at 100 and loses 15 points for every
error and 3 for every warning, never going below 0 โ which is why the single error above scores
85. A draft whose data cannot be parsed at all scores 0.
This call has a persist query parameter and it defaults to true. On the default, the
result is written back onto the draft โ is_valid, validation_errors and validation_score are
replaced, and the draft's status moves:
- A draft that passes becomes
ready. - A draft that fails becomes
draftagain โ unless it is in theexceptionstate, which is left alone, so an imported invoice stays in the exception list until it genuinely passes or is sent.
Send persist=false for a read-only preview. Use that whenever you are only inspecting a draft,
for example when opening a correction screen: with the default, merely looking at an imported
invoice can move it out of the exception list.
Before validating, the call prepares the stored data: it fills the seller in from your saved
company settings if the draft has none, and normalises the payload so it parses into the canonical
invoice model. That is why an imported draft usually comes back with business-rule errors you can
act on, naming the field, rather than one unhelpful parse error. If the data cannot be parsed at
all you get a single error with code PARSE_ERROR.
See Validation for what the rules are and what the error codes mean.
Draft status valuesโ
The values you will see in status are draft, ready, sending, sent, error and
exception. This page describes only the moves it has traced: validation setting ready or
draft, and leaving exception alone. Do not treat the list as a state machine โ read the
status you are given rather than predicting it.
The rest of the drafts APIโ
These calls exist and are listed in the API reference with their parameters and response shapes.
| Call | What it does |
|---|---|
POST /api/v1/drafts | Create a draft โ empty, with invoice data, or from a saved template |
GET /api/v1/drafts | List drafts, paginated |
GET /api/v1/drafts/recent | The most recently updated drafts |
GET /api/v1/drafts/periods | Year and month rollup of invoices by issue date |
PUT /api/v1/drafts/{draft_id} | Replace the draft's invoice data |
PATCH /api/v1/drafts/{draft_id} | Change the draft's name |
DELETE /api/v1/drafts/{draft_id} | Delete one draft |
DELETE /api/v1/drafts/bulk | Delete several drafts in one call |
POST /api/v1/drafts/{draft_id}/duplicate | Copy a draft into a new one |
GET /api/v1/drafts/{draft_id}/pdf | The draft rendered as a PDF |
GET /api/v1/drafts/{draft_id}/ubl | The draft rendered as UBL 2.1 XML |
GET /api/v1/drafts/{draft_id}/audit | Who changed which field, and when |
GET /api/v1/drafts/{draft_id}/audit.csv | The same audit trail as a CSV download |
POST /api/v1/drafts/{draft_id}/send | Send the draft over Peppol |
POST /api/v1/drafts/{draft_id}/send-via-goroute | First email send of the draft โ see Sender domains |
POST /api/v1/drafts/{draft_id}/resend | Send the invoice email again |
GET /api/v1/drafts/{draft_id}/resends | The log of resend attempts |
POST /api/v1/drafts/cleanup | Remove old drafts |
Related pagesโ
- Validation โ the rules a draft is checked against
- Sender domains โ what a draft's email send needs
- Delivery and formats โ what happens when a draft is sent
- Webhooks โ building an approval step out of drafts