Bulk upload
Upload many invoices at once from a spreadsheet — CSV or XLSX.
Five endpoints for one workflow: download a template, upload a filled-in spreadsheet, poll while it validates, optionally release it, and (if anything failed) download a report naming exactly which rows and why. Every invoice a batch produces shows up in List transactions and Get a transaction like any other — bulk upload is a different way in, not a separate system.
Column names differ from the JSON API
The spreadsheet uses human-readable names where Create an
invoice uses codes: invoice_type_code is a word (invoice,
credit_note, …) instead of a UBL type code, and invoice_transaction_type is
a comma-separated list of flag names instead of a 20-character binary string. See the mapping tables
below before assuming the two formats share a vocabulary.
Get the template
/v1/api/invoices/bulk/template| Field | Type | Description | |
|---|---|---|---|
| format | enum | required | csv or xlsx. Anything else is a 400 |
Both formats share the same columns. The response is the file itself — text/csv or the XLSX
content type — not a JSON envelope.
Column groups
Eighty-odd columns, none of them optional to know about — grouped by what part of the invoice they fill in. Where a group shares a prefix it's shown once; the columns below it are the part that varies.
Header
Same meaning as the matching top-level fields on Create an invoice
Buyer
buyer.The buyer object
Seller
seller.For self-billed rows
Delivery
delivery.Full object for flag ecommerce, country only for flag export
Import & special zone
For flags import_goods and special_zone
Third party
third_party.For flag third_party
Line item
item.One per row — see Multiple lines below for more than one
invoice_type_code — spreadsheet word vs. JSON code
| Spreadsheet value | Same as JSON's invoice_type_code |
|---|---|
invoice | 380 |
credit_note | 381 |
debit_note | 383 |
prepayment_invoice | 386 |
self_billed_invoice | 389 |
self_billed_credit_note | 261 |
invoice_transaction_type — spreadsheet flag names vs. JSON bit position
A comma-separated list, e.g. full_tax, profit_margin_self. Exactly one of full_tax/simplified is
required, same rule as bit 1/2 on the JSON side — see Create an invoice for
what each flag requires.
| Spreadsheet name | JSON bit |
|---|---|
full_tax | 1 |
simplified | 2 |
self_billed | 3 |
third_party | 4 |
summary | 5 |
continuous_supply | 6 |
export | 7 |
deemed_supply | 8 |
import_rcm | 9 |
profit_margin | 10 |
profit_margin_self | 11 |
ecommerce | 12 |
import_goods | 13 |
special_zone | 14 |
prepayment | 15 |
Multiple lines on one invoice
A row is one line item. For more than one line on the same invoice, repeat the invoice_number on a
second row and leave every column blank except the item.* ones:
invoice_number,...,item.item_name,item.quantity,item.unit_code,item.unit_price,...
EX-02-TWO-LINES,2026-08-27,10:00:00,invoice,full_tax,OMR,...,"Portland cement, 50kg bags",20,C62,5.000,...
EX-02-TWO-LINES,,,,,,...,,Site supervision,8,HUR,25.000,...The first row's header/buyer/seller fields apply to the whole invoice; a continuation row's blank
columns are ignored, not treated as overrides. Poll batch status reports both
physical rows folded into one source_rows: [3, 4] entry against a single invoice_number.
Upload a batch
/v1/api/invoices/bulk| Field | Type | Description | |
|---|---|---|---|
| file | file | required | multipart/form-data, field name file. .csv or .xlsx, max 10 MB |
| sender_id | uuid | required | multipart field, not query — which taxpayer the batch belongs to. A key may act for many taxpayers; a batch belongs to exactly one |
| validate_only | boolean | optional | Query parameter. true checks everything and stages nothing — see the callout below. Default false |
curl -X POST "https://api.joinsumu.com/v1/api/invoices/bulk?validate_only=false" \
-H "X-API-Key: sumu_live_..." \
-F "file=@invoices.csv" \
-F "sender_id=625919e2-a4cf-40a7-8cfd-f8fedb712080"{
"statusCode": 202,
"message": "Batch accepted for validation",
"data": {
"batch_id": "3c4e244f-e95f-4317-8cfd-93a20dd270af",
"status": "VALIDATING",
"filename": "invoices.csv",
"row_count": 21,
"invoice_count": 20,
"validate_only": false,
"created_at": "2026-08-27T11:57:21.948Z"
}
}row_count counts physical spreadsheet rows; invoice_count counts distinct invoice_numbers after
folding continuation rows together — the two disagree exactly when multiple lines
are in play. 202 means accepted for validation, not validated yet — poll for the result.
validate_only checks everything and stages nothing
A validate_only batch runs the identical validation as a real upload — same errors, same
per-row detail — but produces no drafts to release. Calling
Release on one fails with a 400 naming exactly that. Use it to check a file
is clean before committing to it, the same role
Dry run plays for a single JSON payload.
Rejected before validation even starts: no file part, a file that isn't .csv/.xlsx, a file
missing an invoice_number column, or a sender_id that isn't a taxpayer your organisation manages
(403 FORBIDDEN_ERROR, same message as everywhere else — see Errors).
Poll batch status
/v1/api/invoices/bulk/{batch_id}| Field | Type | Description | |
|---|---|---|---|
| sender_id | uuid | required | Query parameter — proves ownership before the batch is read |
| rows | boolean | optional | Include the per-row rows[] array. Omit for just the batch-level counts |
{
"statusCode": 200,
"message": "Batch retrieved",
"data": {
"batch_id": "3c4e244f-e95f-4317-8cfd-93a20dd270af",
"status": "READY",
"filename": "01-template.csv",
"source_format": "csv",
"row_count": 21,
"invoice_count": 20,
"valid_count": 19,
"failed_count": 1,
"sent_count": 0,
"validate_only": false,
"parse_error": null,
"released_at": null,
"created_at": "2026-08-27T11:57:21.948Z",
"updated_at": "2026-08-27T11:58:06.795Z",
"rows": [
{
"row_id": "b2153c4b-cd63-455e-a893-5738d8734aab",
"source_rows": [19],
"invoice_number": "EX-17-PROFIT-MARGIN-SELF",
"status": "DRAFTED",
"transaction_id": "8c7a96fc-4fab-494d-810f-437750561bcf",
"issues": []
},
{
"row_id": "12e89f9e-63ce-4cdd-9e09-d9a1c89e4631",
"source_rows": [22],
"invoice_number": "EX-20-DELIBERATELY-INVALID",
"status": "INVALID",
"transaction_id": null,
"issues": [
{
"severity": "error",
"code": "VALIDATION_ERROR",
"rule_id": "CL-08-OM-HS",
"message": "Unknown item classification code(s): 840110. IBT-158 must be a 12-digit Oman HS code from the published list (CL-08-OM-HS)",
"location": null
}
]
}
]
}
}Batch status
| Value | Meaning |
|---|---|
VALIDATING | Just uploaded, being checked row by row |
READY | Validation finished. valid_count rows drafted, failed_count rows will never send until fixed and re-uploaded |
RELEASING | Release called, drafts are being sent |
COMPLETED | Every releasable row has been sent (or already failed validation) |
Row status
| Value | Meaning |
|---|---|
DRAFTED | Passed validation, saved as a draft (same as save_as_draft on Create an invoice), waiting for release |
SENT | Released — transaction_id tracks it from here exactly like any other invoice |
INVALID | Failed validation. issues[] is the same shape Dry run uses; transaction_id is null — nothing was created |
A 404 NOT_FOUND_ERROR ("Batch not found") means either the id is wrong or it belongs to a taxpayer
outside sender_id — the two aren't distinguished, same convention as everywhere else on this API.
Download the error report
/v1/api/invoices/bulk/{batch_id}/errors.csvOne row per INVALID row in the batch, naming which source rows and why:
source_rows,invoice_number,error
22,EX-20-DELIBERATELY-INVALID,CL-08-OM-HS: Unknown item classification code(s): 840110. IBT-158 must be a 12-digit Oman HS code from the published list (CL-08-OM-HS)Release the batch
/v1/api/invoices/bulk/{batch_id}/release| Field | Type | Description | |
|---|---|---|---|
| sender_id | uuid | required | Body field this time, not query — proves ownership before releasing |
Sends every DRAFTED row. INVALID rows are left exactly as they are — releasing doesn't retry them,
fix the file and upload a new batch instead. Moves the batch to RELEASING immediately (202); poll
for COMPLETED.
Three ways this is refused, all 400 VALIDATION_ERROR:
- Called on a batch that's still
VALIDATING— the message names the batch id to poll. - Called twice — a batch already
RELEASING(or past it) can't be released again. - Called on a
validate_onlybatch — see the callout above; nothing was ever staged to send.
List batches
/v1/api/invoices/bulk| Field | Type | Description | |
|---|---|---|---|
| sender_id | uuid | required | Same convention as List transactions — one taxpayer per call |
| limit | integer | optional | Default and paging behaviour match List transactions |
| offset | integer | optional |
Same { docs, total_docs, limit, offset } envelope as
List transactions, newest first. Each entry is the batch-level summary —
call Poll batch status with rows=true for the per-invoice detail.