What reading OpenFeed data actually costs, and how often it is worth doing.

You are charged per grant, not per call

This is the single most important thing to know, and it is the opposite of what most metered APIs condition you to expect.

OpenFeed charges 1 credit per active consumer↔app grant, per calendar month. Charging is per relationship, not per request:

  • The first credit is debited immediately when a grant is created.
  • One more is debited on each calendar-month anniversary, while the grant is active.
  • Per-request and per-record pricing are explicitly not part of the model.
  • New developers get 10 credits automatically on first app registration.

So the number of API calls you make is not what costs you money. The number of grants you hold is.

What follows from that

Do not contort your design to minimise request counts. Fetching accounts, then a balance and a year of transactions for each, is not more expensive than fetching accounts alone. Re-syncing hourly costs the same as re-syncing daily.

Do care about grants you no longer use. Every grant is a recurring monthly charge, whether or not you read from it. If a consumer stops using your product, revoking their grant stops the charge:

{:ok, grant} = AshOpenFeed.revoke(grant)

There is no proration. A grant revoked mid-month is not refunded for the remainder, deliberately — so nothing incentivises churning grants to game the billing. Revoking early in a month saves nothing for that month; it prevents the next anniversary charge.

There is no dormancy state. Grants charge on a fixed calendar schedule regardless of activity. A grant you never read from costs exactly as much as one you read hourly. There is no inactivity grace window and no reactivation signal: a grant is ACTIVE or SUSPENDED, and nothing else.

When credit runs out

Credits cannot go negative — it is enforced by a database constraint, not just application logic. When a developer's balance cannot fund a charge, the grant is suspended and every data call returns:

{:error, %OpenFeed.Error{kind: :credit_exhausted, status: 402}}

That is your problem, not the consumer's — their consent is still perfectly valid. Handle it distinctly from a revocation:

case OpenFeed.Sharing.banking_accounts(config, tokens) do
  {:error, %OpenFeed.Error{kind: :credit_exhausted}} ->
    # Top up. Do not mark the grant revoked; do not ask the consumer to
    # reconnect — there is nothing wrong with the grant.
    :suspend_syncing

  {:error, %OpenFeed.Error{kind: :grant_revoked}} ->
    # Actually terminal.
    :mark_revoked
end

ash_openfeed does this for you: AshOpenFeed.with_token/3 marks the grant's metering_state as :suspended on a 402 and leaves status alone.

What is worth being careful about

Calls are free, but they are not instant, and two things deserve restraint.

Balances hit the data holder

Balance endpoints are not served from OpenFeed's mirror. They are fetched from the upstream data holder on demand and cached for roughly 15 minutes. That makes them the slowest calls available and the only ones that routinely fail while everything else works:

{:error, %OpenFeed.Error{kind: :balance_unavailable, status: 502}}

Fetch a balance when you are about to show it, not once per account in a nightly loop over your whole user base. It is retryable, and a failure should never abandon the surrounding sync.

Everything else is mirrored, and refreshed on OpenFeed's schedule

Accounts, transactions, meters, usage, invoices and billing are served from OpenFeed's own store, which it refreshes from data holders on a cadence of roughly 4 hours for banking and 6 hours for energy.

That is the practical ceiling on useful polling. Reading every 5 minutes is not billed differently, but it cannot surface data that OpenFeed has not fetched yet — you will get the same bytes back.

A cadence that makes sense

# Every 4 hours, matching OpenFeed's banking refresh.
config :my_app, Oban,
  plugins: [
    {Oban.Plugins.Cron, crontab: [{"0 */4 * * *", MyApp.OpenFeed.SyncAllWorker}]}
  ]

With, per grant:

  • transactions and usage — every run, with a ~7 day overlap so late-posted records land. Upsert on the provider's id, so re-reading is free.
  • accounts and meters — every run; they are small and they do change (accounts close, meters get replaced).
  • balances — on demand when a user is looking, or once per run if you need them stored. Not in a tight loop.
  • invoices and billing — daily is plenty. They change slowly.

Noticing revocations without waiting to fail

A consumer can withdraw consent at any time, and the default way you find out is a 403 on your next data call. If you would rather know sooner, poll the app-level grant index with a client-credentials token:

{:ok, tokens} = OpenFeed.Auth.client_credentials_token(config, [:grant_list])
{:ok, grants} = OpenFeed.Sharing.app_grants(config, tokens)

It returns a lightweight id / revision / lastUpdated index of every grant your app holds, so you can reconcile against your own records cheaply. This costs no credits — it is not a per-grant data call.

With grant_management? true on your resource you can also ask about one grant directly:

{:ok, grant} = AshOpenFeed.refresh_status(grant)

which reconciles status and metering_state from OpenFeed in one call.

Token lifetimes, which are not a cost but do shape scheduling

  • Access tokens are short-lived. AshOpenFeed.access_token/2 refreshes automatically when one is within 120 seconds of expiry.
  • Refresh tokens are not rotated, and under the Recommended profile live as long as the grant does. A refresh returns a new access token and leaves your refresh token unchanged.

Because they are not rotated, two concurrent refreshes both succeed and neither invalidates the other — concurrent refresh is wasteful, not destructive. If you fan a sync out across many grants and want to avoid the waste, give the worker a uniqueness constraint rather than reaching for a database lock.

Sources

The metering model above is OpenFeed's documented behaviour rather than this library's invention. See https://openfeed.au/developers for current pricing and the developer terms.