Zum Hauptinhalt springen

Export and Delivery

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

Vodia Billing 61 Vodia Billing 61

What is exported​

Export emits typed rows, one per charge:

TypeSource
USAGERated calls
RECURRINGSeats, DIDs, licences
ONE_TIMECatalogue 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:

#ColumnNotes
1record_typeUSAGE, RECURRING, ONE_TIME, DISCOUNT or CREDIT
2systemThe PBX the charge came from
3tenant_domainThe site, always — never the customer
4period_or_callidThe billing period, or the call id on a USAGE row
5call_start_utcUSAGE rows only
6direction_or_subtypeO on usage; the subtype or SKU otherwise
7number_or_descThe far-end number, or the description
8destinationRate destination
9secondsCharged seconds
10quantityUnits billed
11includedY when an allowance covered the call
12amountExtended sell amount, four decimal places
13currency
14account_refThe billing party's reference
15rate_class
16customer_idThe billing party's stable id
17customer_nameThe 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.