Events
Every request you send is answered by at least one of the first four events, and every retrieval run by one of the last two. All of them carry schema_version and request_id. The request events also carry client_reference when you sent one; the retrieval events carry connection_id.
| Event | When | Key it by | What to do |
|---|---|---|---|
statement.available |
A statement was extracted, or a new version of one | version_id |
Store it under statement_id, replacing any earlier version |
statement.withdrawn |
A re-extraction no longer contains a statement you were sent | withdrawal_id |
Stop using that statement_id. It is never sent again |
request.failed |
The request produced no statements | failure_id |
Act on reason.customer_action_required |
request.duplicate |
The file was already sent under another request | duplicate_id |
Follow the earlier request named in duplicate_of |
retrieval.completed |
A run reached the portal and listed its accounts | event_id |
Update your accounts; act on new_accounts and missing_accounts |
retrieval.failed |
A run did not complete | event_id |
Act on reason.customer_action_required |
request.failed
{
"schema_version": "1.0.0",
"event": "request.failed",
"failure_id": "flr_6c2e9a17-4b83-4f05-9d61-2a7e0c5b8f39",
"request_id": "req_0a4d6e82-9c37-4b15-a8f2-3e7b1d5c9042",
"client_reference": "invoice-433193",
"failed_at": "2026-09-24T03:41:12Z",
"reason": {
"code": "not_a_pdf",
"message": "The document received is not a PDF. Resubmit the bill as a PDF.",
"customer_action_required": true
}
}
reason.code |
customer_action_required |
Meaning |
|---|---|---|
not_a_pdf |
true | The file is not a PDF. Resubmit the bill as a PDF |
unreadable_pdf |
true | The PDF is damaged or cannot be opened. Resubmit a readable copy |
extraction_failed |
false | We could not extract the bill completely. It is held for review, not sent incomplete |
validation_failed |
false | The extraction failed our checks and was not sent |
internal_error |
false | A fault on our side |
A failure is not final. When customer_action_required is false the problem is ours: we fix it and re-run the request, and its statements then arrive under the same request_id, after the failure. No further event retracts the failure: a statement under that request_id supersedes it. Codes may be added, so treat an unknown code by its customer_action_required flag.
statement.withdrawn
Statement ids follow a bill's content across re-extractions. If a re-extraction no longer contains a statement, for example because a summary bill was previously split one child too many, the statement is withdrawn. You are told only about statements you were actually sent, once each. A withdrawal is normally sent after the request's current statements, but if a delivery to you fails partway it can arrive first, with the replacements following on redelivery. Withdrawing a summary parent does not withdraw its children: each statement is withdrawn, or kept, on its own.
{
"schema_version": "1.0.0",
"event": "statement.withdrawn",
"withdrawal_id": "wdr_3a7e1c95-2d48-4f06-b913-8c5e0a7d4f21",
"statement_id": "stt_b5e29c74-1f83-4a06-8d51-7e0c3b9d4a26",
"last_version_id": "ver_7f03d8a1-6b29-4c54-9e18-0d5a2f7c3b96",
"request_id": "req_4c8e2a17-6b93-4d05-8f21-7e0a3c9d5b68",
"client_reference": "invoice-604882",
"withdrawn_at": "2026-09-29T21:14:05Z",
"reason": {
"code": "not_in_reextraction",
"message": "A re-extraction of this request no longer contains this statement. Stop using it; the request's current statements have been sent."
}
}
last_version_id is the last version you received. If the same bill reappears later, it arrives under a new statement_id.
request.duplicate
{
"schema_version": "1.0.0",
"event": "request.duplicate",
"duplicate_id": "dup_6e2b9f14-8c37-4a50-b1d2-5f09c3e7a846",
"request_id": "req_9d15a3e8-4f27-4b61-8c09-2e7d5b1f6a43",
"client_reference": "invoice-433193-resend",
"duplicate_of": {
"request_id": "req_0a4d6e82-9c37-4b15-a8f2-3e7b1d5c9042",
"client_reference": "invoice-433193"
},
"document_id": "doc_b72e4f18-6a93-4d51-8027-3e9c1b5d8a64",
"detected_at": "2026-09-30T18:02:44Z",
"message": "This request carries a file you already sent. It was not extracted again: its statements are delivered under the earlier request named in duplicate_of."
}
It is sent whether the earlier request has finished, is still running or failed. Follow that request's own events.
retrieval.completed
The run reached the portal and listed the login's accounts. A run that finds no new bill still completes; that is the normal case on most days of a month.
{
"schema_version": "1.0.0",
"event": "retrieval.completed",
"event_id": "rtv_3f1c7a52-8d4e-4b9a-a6f0-2c9e81d4b7aa",
"request_id": "run_437971a5-fd17-41d6-b2ba-bd06485099aa",
"provider": "Example Electric Cooperative",
"completed_at": "2026-10-09T04:12:33Z",
"dry_run": false,
"invoices_found": 2,
"invoices_downloaded": 1,
"accounts": [
{
"account_number": "1000000101",
"account_number_as_printed": "1000-0001-01",
"service_address": "100 Example Street, Springfield",
"most_recent_statement_date": "2026-09-28",
"most_recent_total_due": "$142.17",
"account_id": "acc_1a2b3c4d-0000-4000-8000-000000000001",
"collection": "active",
"status": "connection_success",
"status_detail": "data_accessible"
},
{
"account_number": "1000000201",
"account_number_as_printed": "1000-0002-01",
"service_address": "200 Example Avenue, Springfield",
"most_recent_statement_date": "2026-09-30",
"most_recent_total_due": "$87.40",
"account_id": "acc_1a2b3c4d-0000-4000-8000-000000000002",
"collection": "never_collected",
"status": "connection_success",
"status_detail": "data_accessible"
}
],
"connection_id": "con_8e4b1d2a-6f73-4c09-b5e1-9d0a7c3f2e64",
"new_accounts": [
{ "account_id": "acc_1a2b3c4d-0000-4000-8000-000000000002", "account_number": "1000000201",
"account_number_as_printed": "1000-0002-01", "collection": "never_collected",
"status": "connection_success", "status_detail": "data_accessible" }
],
"missing_accounts": [
{ "account_id": "acc_1a2b3c4d-0000-4000-8000-000000000003", "account_number": "1000000301",
"account_number_as_printed": "1000-0003-01", "collection": "active",
"status": "connection_error", "status_detail": "not_listed_by_portal" }
],
"enumeration": { "expected_account_count": 3, "enumerated_account_count": 2, "shortfall": false }
}
request_idis the run's:run_plus a UUID for a regular run you triggered,dsc_plus a UUID for a discovery run you asked for (a connection's first run is a discovery run whatever you asked for, and keeps therun_prefix if you asked for a regular one),sch_plus a UUID for a scheduled one.- One run can send more than one event. A run that could not read the portal (
portal_unavailable,internal_error) or saw too few accounts is retried under the samerequest_id, and each attempt sends its own event with a newevent_id. The latest, bycompleted_atorfailed_atcompared as a time, is the run's outcome. dry_runis true for the discovery run, which lists accounts and downloads nothing.accountsis every account this run listed.new_accountsare those seen for the first time;missing_accountsare those listed before and absent now.enumeration.shortfalltrue means the run saw too few accounts to trust, and nothing was marked missing.enumerationis absent when the run listed no accounts at all.invoices_foundcounts what the portal listed in the window, andinvoices_downloadedwhat we downloaded. Bills for accounts you do not collect are held back before delivery.most_recent_total_dueis the portal's own text, not a parsed amount; the statement carries the amount as a value.
retrieval.failed
The run did not complete. reason has the same shape as on request.failed.
{
"schema_version": "1.0.0",
"event": "retrieval.failed",
"event_id": "rtv_9c4e2b71-3a5d-4f86-8e10-b7d2c6a1f053",
"request_id": "sch_5c0e2d71-9b84-4a36-8f15-d27e6a0c3b91",
"provider": "Example Electric Cooperative",
"failed_at": "2026-10-09T04:12:33Z",
"reason": {
"code": "credentials_failed",
"message": "The portal rejected the username or password.",
"customer_action_required": true
},
"dry_run": false,
"invoices_found": 0,
"invoices_downloaded": 0,
"accounts": [],
"connection_id": "con_8e4b1d2a-6f73-4c09-b5e1-9d0a7c3f2e64",
"new_accounts": [],
"missing_accounts": []
}
reason.code |
customer_action_required |
What to do |
|---|---|---|
credentials_failed |
true | Send the correct password (PUT .../credentials). We do not run the connection until you do |
mfa_failed |
true | Make sure the portal's codes reach the connection's mfa.address, then send the credentials again |
client_assistance_required |
true | The portal wants the account holder to act (accept terms, confirm details). Do that on the portal, then send the credentials again |
provider_not_enabled |
true | This portal is not enabled for your account, so the run was not attempted. Contact us |
portal_unavailable |
false | The portal was down or showed no billing data. Nothing to do; we retry |
internal_error |
false | Ours. Nothing to do; we retry |
As with request.failed, codes may be added. Treat an unknown code by its customer_action_required flag.
Statements from a connection
A retrieved bill arrives as an ordinary statement.available, before or after its run's event, under a request_id of its own (one per bill, not the run's). It also carries connection_id, and account_id for the account we retrieved it for, even where the bill prints its account number differently from the portal. A bill we cannot match to one of the connection's accounts is delivered with connection_id and no account_id. A bill for an account you have not activated, or have deactivated, is not delivered.