Export and Delivery
Export produces CSV files for a payment system or an external biller. Delivery decides where those files go.

What is exported
Export emits typed rows, one per charge:
| Type | Source |
|---|---|
USAGE | Rated calls |
RECURRING | Seats, DIDs, licences |
ONE_TIME | Catalogue and one-off charges |
Included calls are exported as 0.00 rows rather than omitted, so the receiving system sees the traffic.
Rows carry each charge's own sell amount. Export never writes invoice totals and never writes tax — it is a charge feed, not an invoice feed.
Columns
Every file has the same seventeen columns, in this order, on every row regardless of type:
| # | Column | Notes |
|---|---|---|
| 1 | record_type | USAGE, RECURRING, ONE_TIME, DISCOUNT or CREDIT |
| 2 | system | The PBX the charge came from |
| 3 | tenant_domain | The site, always — never the customer |
| 4 | period_or_callid | The billing period, or the call id on a USAGE row |
| 5 | call_start_utc | USAGE rows only |
| 6 | direction_or_subtype | O on usage; the subtype or SKU otherwise |
| 7 | number_or_desc | The far-end number, or the description |
| 8 | destination | Rate destination |
| 9 | seconds | Charged seconds |
| 10 | quantity | Units billed |
| 11 | included | Y when an allowance covered the call |
| 12 | amount | Extended sell amount, four decimal places |
| 13 | currency | |
| 14 | account_ref | The billing party's reference |
| 15 | rate_class | |
| 16 | customer_id | The billing party's stable id |
| 17 | customer_name | The billing party's name as it reads today |
Columns are only ever appended, never inserted. A parser reading by position keeps working across upgrades, and one reading by header name picks new columns up on its own.
Who the row is billed to
Columns 14, 16 and 17 answer one question: which party pays for this row.
A customer can span many
sites and more than one PBX, and it receives one invoice covering all of
them. In the export those sites are still separate rows — the file reports
what each site consumed, which is what makes it useful to a margin report and
to a biller — but every row now names the party it will be invoiced under.
Eleven sites on one invoice are eleven rows carrying one customer_id.
account_ref is the party's reference, not the site's. Where a party has
none of its own, the row falls back to the site's own reference, so a value
that was present before an upgrade is never replaced by a blank.
Take both customer_id and customer_name if you are joining on them.
The name is what a person reconciling against an invoice reads; the id is what
survives the customer being renamed.
A customer column is a label on a row, not a grouping. The file remains one row per charge and still says what each site earned.
Service rows
Services are typed
RECURRING, but a service's subtype is an internal assignment id, which is of
no use to a biller. Those rows carry the catalogue SKU in
direction_or_subtype and the description with its label in number_or_desc
instead:
"RECURRING","a41f","hq.northwind-example.com","2026-11","","FTTC80","NBN 100 — Head office · AVC10000000","","","1","","99.0000","USD","NW-1041","","65f2","Northwind Group"
Two circuits on one tenant are two rows, distinguished by the label — the same distinction the invoice and the accounting hand-off make.
The manual export and the scheduled export emit identical rows; they share one row builder rather than two copies of one.
Export is sell-only by construction. Cost prices and margin cannot reach an export file.
Properties
- Watermarked on
rated_at, never on call date, so a recovery import of last month's calls is exported once and only once. - Split per currency automatically. A fleet billing in three currencies produces three files.
- sha256 manifest alongside each file.
- Immutable. A generated file is never regenerated. A retry re-sends the stored bytes, so the receiving system can never get two different files under one sequence number.
- Sequenced. Each export carries a sequence number, recorded per charge as
exported_seq.
Delivery destinations
Delivery configures where export files are sent. Three transports:
SFTP — uploaded as .part and then renamed, so a watcher on the far side
never reads a half-written file. Host, port, user, password or key, and remote
path.
HTTP — POST or PUT with the CSV as the request body, and headers the
receiver can verify against: X-Content-SHA256, X-Filename, X-Currency
and X-Row-Count. Optional bearer token.
Email — the CSV as an attachment to one or more addresses.
Destinations marked deliver automatically receive every new export. Anything can also be delivered or retried by hand.
Attempt tracking
Every attempt is recorded per destination and per file on the export record. A delivery where some files succeeded and others failed is reported as partial, and a retry is limited to the files that actually failed.
Exported but unbilled
The charge_exported_unbilled detector finds charges that were exported but
never landed on an invoice. Those charges are otherwise permanently invisible:
the export watermark has passed them, so they will not be exported again, and
no invoice carries them.
If this alert fires, the charges need to be identified and handled manually. It is the one detector whose findings represent money that is silently gone.
Worked example
The file
One invoice's charges as an export file. Sell-only, no tax and no invoice totals. Usage is one row per call, not a per-destination rollup — the rollup is an invoice concept, and this file is the evidence behind it:
record_type,system,tenant_domain,period_or_callid,call_start_utc,direction_or_subtype,number_or_desc,destination,seconds,quantity,included,amount,currency,account_ref,rate_class,customer_id,customer_name
USAGE,a41f,hq.northwind-example.com,3f2a91@pbx,2026-08-14T09:12:44.000Z,O,441617000123,United Kingdom,86,1,,3.0100,USD,NW-1041,standard,65f2,Northwind Group
USAGE,a41f,hq.northwind-example.com,3f2a97@pbx,2026-08-14T09:31:02.000Z,O,447700900123,UK Mobile,12,1,,1.6800,USD,NW-1041,standard,65f2,Northwind Group
USAGE,a41f,hq.northwind-example.com,3f2aa4@pbx,2026-08-14T10:04:19.000Z,O,18005550199,US Toll Free,318,1,Y,0.0000,USD,NW-1041,standard,65f2,Northwind Group
RECURRING,a41f,hq.northwind-example.com,2026-08,,ext:plain,,,,42,,504.0000,USD,NW-1041,,65f2,Northwind Group
RECURRING,a41f,hq.northwind-example.com,2026-08,,did,,,,18,,27.0000,USD,NW-1041,,65f2,Northwind Group
ONE_TIME,a41f,hq.northwind-example.com,2026-08,,hardware,Desk handset,,,1,,89.0000,USD,NW-1041,,65f2,Northwind Group
DISCOUNT,a41f,hq.northwind-example.com,2026-08,,tenant,Promo10,,,1,,-62.3800,USD,NW-1041,,65f2,Northwind Group
The toll-free row is present at 0.0000 rather than omitted, so the receiving
system sees the traffic. It carries Y in included because an allowance
covered it.
One customer, several sites
Where a customer covers more than one site, each site keeps its own rows and they all name the same party. This is the file behind a single invoice covering three branches:
record_type,system,tenant_domain,period_or_callid,call_start_utc,direction_or_subtype,number_or_desc,destination,seconds,quantity,included,amount,currency,account_ref,rate_class,customer_id,customer_name
RECURRING,a41f,hq.northwind-example.com,2026-08,,ext:plain,,,,42,,504.0000,USD,NW-1041,,65f2,Northwind Group
RECURRING,a41f,leeds.northwind-example.com,2026-08,,ext:plain,,,,11,,132.0000,USD,NW-1041,,65f2,Northwind Group
RECURRING,b902,dublin.northwind-example.com,2026-08,,ext:plain,,,,7,,84.0000,EUR,NW-1041,,65f2,Northwind Group
DISCOUNT,a41f,leeds.northwind-example.com,2026-08,,customer,Volume 5%,,,1,,-36.0000,USD,NW-1041,,65f2,Northwind Group
Three sites, two PBXs, one customer_id and one account_ref. The discount
is written once, against the site it was earned on, rather than repeated
across every member.
The Dublin row is in EUR, so it lands in a different file — currencies split, and a customer cannot span currencies in the first place.
A multi-currency export
A fleet billing in three currencies produces three files from one export, and a manifest:
export-1041-USD.csv 412 rows sha256 3f9a...c218
export-1041-EUR.csv 88 rows sha256 71bd...04ef
export-1041-GBP.csv 34 rows sha256 aa19...9d5c
export-1041.manifest (all three checksums)
Delivery, and a partial
Export 1041, 3 files, 2 destinations
sftp://biller.example.com/upload
export-1041-USD.csv delivered 2026-09-01 03:14:02
export-1041-EUR.csv delivered 2026-09-01 03:14:05
export-1041-GBP.csv delivered 2026-09-01 03:14:07
https://api.partner-example.com/charges
export-1041-USD.csv delivered 2026-09-01 03:14:11
export-1041-EUR.csv FAILED 504 Gateway Timeout
export-1041-GBP.csv FAILED 504 Gateway Timeout
Status: PARTIAL
A retry touches only the two failed files, and re-sends the stored bytes rather than regenerating them. The biller can never receive two different files under one sequence number.
The HTTP headers a receiver can verify
POST /charges HTTP/1.1
Host: api.partner-example.com
Authorization: Bearer <token>
Content-Type: text/csv
X-Filename: export-1041-USD.csv
X-Content-SHA256: 3f9a...c218
X-Currency: USD
X-Row-Count: 412
The body is the CSV itself. A receiver that checks X-Content-SHA256 against
what it read cannot silently ingest a truncated file.
The SFTP rename
Files are uploaded as .part and then renamed:
put export-1041-USD.csv.part
rename export-1041-USD.csv.part -> export-1041-USD.csv
A watcher on the far side polling for *.csv therefore never reads a
half-written file.
Watermarking, and why recovery does not double-export
Export selects on rated_at, never on call date.
2026-09-01 export 1041 runs, watermark advances to 2026-09-01 03:00
2026-09-04 archive CSV for 2026-08-19 imported, 240 legs rated
2026-10-01 export 1042 runs
-> includes the 240 legs from 2026-08-19
(rated 2026-09-04, after the previous watermark)
Those legs appear on the August invoice, because invoice periods are cut on when the call started. But they export in the October file, because export tracks when they were rated. Both are correct, and they are answering different questions.