Docs: Connecting a portal

Connecting a portal

A connection is one login to one utility portal. We store the login encrypted, sign in on a schedule or when you ask, list every account the login can see, and retrieve bills only for the accounts you activate. Accounts are covered on their own page: Accounts.

  1. Register the connection with POST /v1/connections. It starts as pending_verification.
  2. The first run verifies the login and lists its accounts. It downloads nothing: its retrieval.completed has "dry_run": true and every account under new_accounts. It follows the schedule, or you can start it at once with POST /v1/connections/{connection_id}/runs.
  3. Activate the accounts you want, or send their numbers beforehand with PUT /v1/expected-accounts and they activate themselves when found. Accounts you do not activate stay never_collected, and their bills are never delivered.
  4. We retrieve each active account around its billing cycle. Every run ends in a retrieval.completed or retrieval.failed event, and each new bill arrives as a statement.available.

Endpoints

The connection API shares the base URL https://api.luminetric.com. Every call carries Authorization: Bearer <your connection API token>, a token issued with your account separately from the upload token. Bodies are JSON, at most 64 KB.

Method and path What it does
GET /v1/providers The portals you can register, each with its credential_fields (username and password today)
POST /v1/connections Registers a login: 201 for a new connection, 200 with the existing one if you registered this login before
GET /v1/connections Every connection on your account, with its status, as a connections array. Each connection's accounts come from its /accounts endpoint
GET /v1/connections/{connection_id} The connection and its status
PUT /v1/connections/{connection_id}/credentials Replaces the password; the next run re-verifies the login
POST /v1/connections/{connection_id}/runs Runs the connection now (see Triggering a run)
DELETE /v1/connections/{connection_id} Deletes the connection and destroys its stored login: 204
GET /v1/connections/{connection_id}/accounts The connection and every account behind it
POST /v1/accounts/{account_id}/activate Starts retrieving an account
POST /v1/accounts/{account_id}/deactivate Stops retrieving it. Statements already sent are unaffected
PUT /v1/expected-accounts Account numbers, for one provider, to activate automatically when a run finds them

Registering a connection

provider is a name from GET /v1/providers. client_reference is your own label, up to 255 characters, returned on the connection.

{
  "provider": "Example Electric Cooperative",
  "credentials": { "username": "[email protected]", "password": "..." },
  "client_reference": "site-114"
}

The answer is the connection. API responses leave out any field that has no value yet, such as last_success_at before the first run.

{
  "connection_id": "con_8d62c46f-d186-427c-9610-2711def78c57",
  "provider": "Example Electric Cooperative",
  "client_reference": "site-114",
  "status": "pending_verification",
  "customer_action_required": false,
  "created_at": "2026-10-13T19:54:44Z",
  "mfa": { "delivery": "email", "address": "[email protected]" },
  "created": true
}

Registering the same login again (same portal, same username, compared ignoring case) returns the existing connection with 200 and stores the password you sent. It is a way to update a password when you do not have the connection_id to hand. To change the username, register a new connection: PUT .../credentials refuses a different username.

Connection status

customer_action_required on the connection is true exactly when its status waits on you. last_failure_reason names the last failed run's reason code (see retrieval.failed).

status Meaning What to do
pending_verification Registered, or credentials replaced, and not yet verified by a run Nothing; the next run verifies it
active Signed in on the last run that reached the portal Nothing
credentials_failed The portal rejected the username or password Send the correct password with PUT .../credentials
mfa_failed The portal asked for a verification code that did not reach us Check that the code goes to mfa.address, then send the credentials again
client_assistance_required The portal needs the account holder to act, for example to accept new terms Do that on the portal, then send the credentials again
disabled We have stopped running this connection Contact us

While a connection waits on you, we do not run it. Sending the credentials again with PUT .../credentials, even unchanged, returns it to pending_verification and the next run verifies it.

Triggering a run

POST /v1/connections/{connection_id}/runs runs a connection now, outside the schedule. The body is optional. Send nothing, or {"mode": "collect"}, for a regular run. Send {"mode": "discover"} for a discovery run, which lists every account behind the login and downloads nothing; use it to find accounts added since the connection's first run.

{
  "request_id": "run_6e6b4590-8c6a-4b3e-bbaf-7b05ab2c588f",
  "connection_id": "con_8d62c46f-d186-427c-9610-2711def78c57",
  "mode": "collect",
  "status": "queued",
  "created": true
}
  • 202 with "created": true: a run was queued. Its request_id is run_ plus a UUID for a regular run and dsc_ plus a UUID for a discovery run you asked for, and the run's retrieval.completed or retrieval.failed carries it. It starts as soon as capacity allows.
  • 200 with "created": false: a run is already pending, and this is that run, whatever mode you asked for. mode says which it is, and status is queued, or in_progress once it has started.
  • 400: the connection waits on you (see Connection status) or is disabled, or mode is not collect or discover. The error says what to do.
  • 429: you triggered a run of this connection, of either mode, less than 60 minutes ago. The error says how many minutes to wait. Scheduled runs do not count toward this.

A regular run is the same as a scheduled one: it looks for bills from the last 62 days, and a bill already delivered is not delivered again. On a connection that has never run, any run is the discovery run of step 2: mode says discover, and the request_id keeps the prefix of the mode you asked for.

The schedule

Once your account is live, we run each connection without being asked:

  • at once after it is registered, after its credentials are replaced, and after you activate an account (within half an hour);
  • daily from 3 days before an active account's next expected bill until that bill arrives or is 20 days late;
  • weekly otherwise, to find accounts that appeared or disappeared.

Apart from those first cases, a scheduled run happens at most once a day. No run starts alongside another run of the same connection, and never while the connection waits on you.

Verification codes

Where a portal sends one-time codes by email, have them reach the address in the connection's mfa.address, either by setting the login's email to it or with a forwarding rule. The address is yours alone. Codes by text message or authenticator app are not currently supported, but can be based on demand.

Security

  • Credentials are encrypted per connection with a key held in a key-management service, never stored beside the data. They are decrypted only at the moment we sign in, and never written to a log or returned by the API.
  • Deleting a connection destroys its stored credentials.
  • You confirm, in your agreement with us, that you hold each account holder's consent to store their login and retrieve their bills.

Responses and errors

Errors have the upload endpoint's shape: {"status": "error", "error": "..."}.

Status Meaning
200 / 201 / 202 Done. 201: a connection was created. 202: a run was queued
204 Deleted
400 Refused as sent. error says why: a malformed body, a portal not enabled for you, a different username on a credentials update, a connection waiting on you, an unknown run mode
401 No valid bearer token
404 No such connection, account or endpoint. Another client's ids answer the same way
405 That method is not allowed on that path
413 The body is over 64 KB
429 A run was triggered too recently
500 / 503 Our fault or temporarily unavailable; retrying is safe