Docs: Events

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_id is 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 the run_ 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 same request_id, and each attempt sends its own event with a new event_id. The latest, by completed_at or failed_at compared as a time, is the run's outcome.
  • dry_run is true for the discovery run, which lists accounts and downloads nothing.
  • accounts is every account this run listed. new_accounts are those seen for the first time; missing_accounts are those listed before and absent now. enumeration.shortfall true means the run saw too few accounts to trust, and nothing was marked missing. enumeration is absent when the run listed no accounts at all.
  • invoices_found counts what the portal listed in the window, and invoices_downloaded what we downloaded. Bills for accounts you do not collect are held back before delivery.
  • most_recent_total_due is 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.