Order Documents API
Attach the legal documents issued for an order, such as its invoice and credit notes. Easel doesn’t issue these documents itself. An invoicing integration issues them in the merchant’s invoicing software, then attaches each one to its order here, so the merchant finds it on the order page and the buyer on their order’s tracking page.
Easel keeps only a document’s number, the codes printed beside it, and a link to it. Removing a document from an order leaves the document itself where it was issued.
An order holds at most 50 documents. Every order returns its
documents in documents, and attaching, changing, or removing one is
recorded in the order’s history.
Order documents have their own scope, order_documents, apart from
orders, so an invoicing integration can attach documents with
orders:read and order_documents:write, without the right to change
orders.
Endpoints
- List Order Documents -
GET /api/v1/orders/{order_id}/documents/ - Attach Order Document -
POST /api/v1/orders/{order_id}/documents/ - Get Order Document -
GET /api/v1/orders/{order_id}/documents/{document_id}/ - Update Order Document -
PUT /api/v1/orders/{order_id}/documents/{document_id}/ - Remove Order Document -
DELETE /api/v1/orders/{order_id}/documents/{document_id}/
List Order Documents
Retrieve a paginated list of an order’s documents, in the order they were attached.
Endpoint: GET /api/v1/orders/{order_id}/documents/
Scope Required: order_documents:read
Path Parameters
| Parameter | Type | Description |
|---|---|---|
order_id |
string | Order public ID (e.g., ord_d0b6fmv28q6vn14peun0) |
Query Parameters
| Parameter | Type | Description |
|---|---|---|
after |
string | Cursor for pagination. Document public ID to return documents after |
before |
string | Cursor for pagination. Document public ID to return documents before, for paging back |
limit |
integer | Number of documents to return (default 10, max 100) |
Every page comes oldest first, whether you page forward with after or back
with before. The response reports has_more, which is true when another
page lies beyond this one in the direction you’re paging.
after and before cannot be used together.
Request
curl -X GET "https://studio.easel.engineering/api/v1/orders/ord_d0b6fmv28q6vn14peun0/documents/" \
-H "Authorization: Bearer your_api_key_here"
Response
Status: 200 OK
{
"data": [
{
"id": "odc_d0b6fmv28q6vn14peun0",
"kind": "invoice",
"number": "FR 2026/123",
"authority_code": "JJ3C4K5L-123",
"url": "https://invoices.example.com/d/8f2a1c/FR-2026-123.pdf",
"issued_at": "2026-10-01T09:00:00Z",
"refund_id": null,
"external_source": "invoicing",
"external_id": "48211",
"created_at": "2026-10-01T09:00:04Z",
"updated_at": "2026-10-01T09:00:04Z"
},
{
"id": "odc_d0b6fmv28q6vn14peun1",
"kind": "credit_note",
"number": "NC 2026/7",
"authority_code": "JJ3C4K5M-7",
"url": "https://invoices.example.com/d/93bd07/NC-2026-7.pdf",
"issued_at": "2026-10-04T15:30:00Z",
"refund_id": "ref_d0b6fmv28q6vn14peun0",
"external_source": "invoicing",
"external_id": "48390",
"created_at": "2026-10-04T15:30:02Z",
"updated_at": "2026-10-04T15:30:02Z"
}
],
"has_more": false
}
Error Responses
Invalid query parameter
Status: 400 Bad Request
A malformed after or before, both at once, or an invalid limit returns
INVALID_REQUEST_ERROR, with the offending parameter named in the error
message:
{
"error": "Invalid 'limit' parameter: cannot exceed 100",
"code": "INVALID_REQUEST_ERROR"
}
Order or cursor not found
An order that doesn’t exist, or an after or before that names no
document of the order.
Status: 404 Not Found
{
"error": "Resource not found",
"code": "NOT_FOUND"
}
Attach Order Document
Attach a document to an order. Name the document by the system that issued
it, in external_source, and its ID there, in external_id. A store can
attach a document only once: attaching the same external_source and
external_id to the order again returns 409 Conflict with the attached
document’s id, so an integration that lost the first response can
update that document instead.
Endpoint: POST /api/v1/orders/{order_id}/documents/
Scope Required: order_documents:write
Path Parameters
| Parameter | Type | Description |
|---|---|---|
order_id |
string | Order public ID (e.g., ord_d0b6fmv28q6vn14peun0) |
Request Body
{
"kind": "credit_note",
"number": "NC 2026/7",
"authority_code": "JJ3C4K5M-7",
"url": "https://invoices.example.com/d/93bd07/NC-2026-7.pdf",
"issued_at": "2026-10-04T15:30:00Z",
"refund_id": "ref_d0b6fmv28q6vn14peun0",
"external_source": "invoicing",
"external_id": "48390"
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
kind |
string | Yes | invoice, credit_note, debit_note, receipt, or delivery_note (see Document Kinds) |
number |
string | Yes | The document’s legal number as printed, such as FR 2026/123: 1 to 100 printable characters |
authority_code |
string|null | No | A code a tax authority’s rules print beside the number, such as Portugal’s ATCUD: up to 128 printable characters. Omit it, or send null or "", for none |
url |
string | Yes | Link the buyer opens: an absolute https URL with a host and no user name or password, of printable ASCII up to 2048 characters. An international domain goes in its xn-- form |
issued_at |
string | Yes | When the document was issued, as an RFC 3339 timestamp from 2000 on, and not more than a day ahead |
refund_id |
string|null | No | The refund a credit_note documents. It must be one of the order’s refunds, and only a credit note may name one |
external_source |
string | Yes | The system that issued the document, up to 32 lowercase letters, digits, -, or _ |
external_id |
string | Yes | The document’s ID in that system, up to 256 characters |
Text fields are trimmed. The URL’s scheme and host are lowercased, and the
rest is kept as sent, so a signed link still works. issued_at is stored in
UTC to the second.
Request
curl -X POST "https://studio.easel.engineering/api/v1/orders/ord_d0b6fmv28q6vn14peun0/documents/" \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-request-id-12345" \
-d '{
"kind": "invoice",
"number": "FR 2026/123",
"authority_code": "JJ3C4K5L-123",
"url": "https://invoices.example.com/d/8f2a1c/FR-2026-123.pdf",
"issued_at": "2026-10-01T09:00:00Z",
"external_source": "invoicing",
"external_id": "48211"
}'
Optional Headers:
- Idempotency-Key: Unique string to prevent duplicate requests (see Idempotency)
Response
Returns the attached document.
Status: 201 Created
{
"id": "odc_d0b6fmv28q6vn14peun0",
"kind": "invoice",
"number": "FR 2026/123",
"authority_code": "JJ3C4K5L-123",
"url": "https://invoices.example.com/d/8f2a1c/FR-2026-123.pdf",
"issued_at": "2026-10-01T09:00:00Z",
"refund_id": null,
"external_source": "invoicing",
"external_id": "48211",
"created_at": "2026-10-01T09:00:04Z",
"updated_at": "2026-10-01T09:00:04Z"
}
Error Responses
Invalid JSON body
A body that isn’t JSON, or an issued_at that isn’t an RFC 3339 timestamp.
Status: 400 Bad Request
{
"error": "Invalid JSON body",
"code": "INVALID_JSON"
}
Invalid field
A field that is missing or breaks its rule. The message says which:
| Field | error |
|---|---|
kind |
kind must be invoice, credit_note, debit_note, receipt, or delivery_note |
number |
number must be 1 to 100 printable characters |
authority_code |
authority_code must be at most 128 printable characters |
url |
url must be an absolute https URL with a host and no user name or password, of printable ASCII up to 2048 characters |
issued_at |
issued_at must be a time from 2000 on, and not more than a day ahead |
external_source, external_id |
external_source and external_id must be set together; external_source is up to 32 lowercase letters, digits, '-', or '_', and external_id is up to 256 characters |
refund_id on a kind other than credit_note |
only a credit_note documents a refund |
refund_id |
refund_id is not a refund of this order |
Status: 400 Bad Request
{
"error": "number must be 1 to 100 printable characters",
"code": "INVALID_FIELD"
}
Document already attached to this order
This order already holds a document with the same external_source and
external_id, including one a concurrent request attached first. id names
it, so you can update it instead.
Status: 409 Conflict
{
"error": "A record with this external reference already exists",
"code": "DUPLICATE",
"id": "odc_d0b6fmv28q6vn14peun0"
}
Document already attached to another order
Another of the store’s orders holds a document with the same
external_source and external_id. There is no id, since that document
isn’t this order’s.
Status: 409 Conflict
{
"error": "Another order already has a document with this external reference",
"code": "EXTERNAL_REF_TAKEN"
}
Request in flight
A request with the same Idempotency-Key is still running. There is no
id; retry after a short delay.
Status: 409 Conflict
{
"error": "Resource is busy, retry shortly",
"code": "DUPLICATE"
}
Order already holds 50 documents
Status: 409 Conflict
{
"error": "An order can have at most 50 documents",
"code": "CONFLICT"
}
Order not found
Status: 404 Not Found
{
"error": "Resource not found",
"code": "NOT_FOUND"
}
Get Order Document
Retrieve one of an order’s documents.
Endpoint: GET /api/v1/orders/{order_id}/documents/{document_id}/
Scope Required: order_documents:read
Path Parameters
| Parameter | Type | Description |
|---|---|---|
order_id |
string | Order public ID (e.g., ord_d0b6fmv28q6vn14peun0) |
document_id |
string | Document public ID (e.g., odc_d0b6fmv28q6vn14peun0) |
Request
curl -X GET "https://studio.easel.engineering/api/v1/orders/ord_d0b6fmv28q6vn14peun0/documents/odc_d0b6fmv28q6vn14peun0/" \
-H "Authorization: Bearer your_api_key_here"
Response
Status: 200 OK
{
"id": "odc_d0b6fmv28q6vn14peun0",
"kind": "invoice",
"number": "FR 2026/123",
"authority_code": "JJ3C4K5L-123",
"url": "https://invoices.example.com/d/8f2a1c/FR-2026-123.pdf",
"issued_at": "2026-10-01T09:00:00Z",
"refund_id": null,
"external_source": "invoicing",
"external_id": "48211",
"created_at": "2026-10-01T09:00:04Z",
"updated_at": "2026-10-01T09:00:04Z"
}
Error Responses
Document not found
A document or order that doesn’t exist, or a document of another order.
Status: 404 Not Found
{
"error": "Resource not found",
"code": "NOT_FOUND"
}
Update Order Document
Replace an attached document’s fields, such as to fix its link. The request
takes the same fields as Attach Order Document,
except external_source and external_id: they stay as the document was
attached, and are ignored if sent. Every field is replaced, so an omitted
authority_code or refund_id is cleared.
Sending what the document already holds changes nothing: updated_at stays
the same and the order’s history records no change, so an integration can
retry freely.
Endpoint: PUT /api/v1/orders/{order_id}/documents/{document_id}/
Scope Required: order_documents:write
Path Parameters
| Parameter | Type | Description |
|---|---|---|
order_id |
string | Order public ID (e.g., ord_d0b6fmv28q6vn14peun0) |
document_id |
string | Document public ID (e.g., odc_d0b6fmv28q6vn14peun0) |
Request Body
{
"kind": "invoice",
"number": "FR 2026/123",
"authority_code": "JJ3C4K5L-123",
"url": "https://invoices.example.com/d/8f2a1c/FR-2026-123-v2.pdf",
"issued_at": "2026-10-01T09:00:00Z"
}
| Field | Type | Required | Description |
|---|---|---|---|
kind |
string | Yes | As on attach |
number |
string | Yes | As on attach |
authority_code |
string|null | No | As on attach; omitted, null, or "" clears it |
url |
string | Yes | As on attach |
issued_at |
string | Yes | As on attach |
refund_id |
string|null | No | As on attach; omitted or null clears it |
Request
curl -X PUT "https://studio.easel.engineering/api/v1/orders/ord_d0b6fmv28q6vn14peun0/documents/odc_d0b6fmv28q6vn14peun0/" \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-request-id-12345" \
-d '{
"kind": "invoice",
"number": "FR 2026/123",
"authority_code": "JJ3C4K5L-123",
"url": "https://invoices.example.com/d/8f2a1c/FR-2026-123-v2.pdf",
"issued_at": "2026-10-01T09:00:00Z"
}'
Response
Returns the updated document.
Status: 200 OK
{
"id": "odc_d0b6fmv28q6vn14peun0",
"kind": "invoice",
"number": "FR 2026/123",
"authority_code": "JJ3C4K5L-123",
"url": "https://invoices.example.com/d/8f2a1c/FR-2026-123-v2.pdf",
"issued_at": "2026-10-01T09:00:00Z",
"refund_id": null,
"external_source": "invoicing",
"external_id": "48211",
"created_at": "2026-10-01T09:00:04Z",
"updated_at": "2026-10-02T11:12:40Z"
}
Error Responses
Invalid JSON body
Status: 400 Bad Request
{
"error": "Invalid JSON body",
"code": "INVALID_JSON"
}
Invalid field
The same field errors as on attach, except those
for external_source and external_id.
Status: 400 Bad Request
{
"error": "refund_id is not a refund of this order",
"code": "INVALID_FIELD"
}
Document not found
Status: 404 Not Found
{
"error": "Resource not found",
"code": "NOT_FOUND"
}
Remove Order Document
Take a document off its order. Only the attachment is removed: the document
stays where it was issued, and the order’s history keeps a
document_removed event naming it.
Endpoint: DELETE /api/v1/orders/{order_id}/documents/{document_id}/
Scope Required: order_documents:write
Path Parameters
| Parameter | Type | Description |
|---|---|---|
order_id |
string | Order public ID (e.g., ord_d0b6fmv28q6vn14peun0) |
document_id |
string | Document public ID (e.g., odc_d0b6fmv28q6vn14peun0) |
Request
curl -X DELETE "https://studio.easel.engineering/api/v1/orders/ord_d0b6fmv28q6vn14peun0/documents/odc_d0b6fmv28q6vn14peun0/" \
-H "Authorization: Bearer your_api_key_here"
Response
Status: 204 No Content
No response body is returned.
Error Responses
Document not found
Status: 404 Not Found
{
"error": "Resource not found",
"code": "NOT_FOUND"
}
Order Document Object
Fields
| Field | Type | Description |
|---|---|---|
id |
string | Document public ID |
kind |
string | invoice, credit_note, debit_note, receipt, or delivery_note |
number |
string | The document’s legal number as printed |
authority_code |
string|null | A code a tax authority’s rules print beside the number, such as Portugal’s ATCUD, or null |
url |
string | The https link the buyer opens |
issued_at |
string | ISO 8601 timestamp of when the document was issued |
refund_id |
string|null | Public ID of the refund a credit note documents, or null |
external_source |
string | The system that issued the document. Unlike other resources’ external references, it’s always set |
external_id |
string | The document’s ID in that system, always set |
created_at |
string | ISO 8601 timestamp of when it was attached |
updated_at |
string | ISO 8601 timestamp of its last change |
Document Kinds
The kinds follow EU VAT practice:
invoice- An invoice, full or simplified, including one that is also a receipt (VAT Directive, Articles 220 to 226b)credit_note- Amends an invoice down, as for a refund (Article 219). It can name the refund it documents inrefund_iddebit_note- Amends an invoice up, as for a charge added after it was issuedreceipt- Acknowledges a payment apart from the invoice, as for an invoice paid laterdelivery_note- Travels with the goods