A failed OpenFeed API call.
Match on :kind, not on :code
:code is the raw machine-readable string OpenFeed returned. :kind is this
library's classification of it, and is what you should branch on — it is
stable across wording changes upstream, and it collapses the cases where two
different statuses mean the same thing to a caller.
case OpenFeed.Sharing.banking_accounts(config, token) do
{:ok, accounts} ->
accounts
{:error, %OpenFeed.Error{kind: :grant_revoked}} ->
# The consumer withdrew consent. Stop syncing; ask them to reconnect.
{:error, %OpenFeed.Error{kind: :credit_exhausted}} ->
# Your OpenFeed credit ran out. Nothing to do with this consumer.
{:error, %OpenFeed.Error{kind: kind}} when kind in [:server_error, :transport_error] ->
# Transient. Try again later.
endWhy this distinction matters
OpenFeed returns HTTP 403 for two unrelated situations:
disclosure_grant_required (the grant is gone or inactive — the consumer
revoked consent) and subject_mismatch (the token's sub does not match the
grant's user — a bug on your side, or a token mix-up).
Treating both as "revoked" will mark healthy grants dead. So the two get
distinct kinds, :grant_revoked and :subject_mismatch, and only the first
means stop syncing.
Kinds
| Kind | Typically | Retry? |
|---|---|---|
:grant_revoked | 403 disclosure_grant_required | No — needs re-consent |
:credit_exhausted | 402 credit_exhausted | No — needs a top-up |
:subject_mismatch | 403 subject_mismatch | No — a bug |
:forbidden | 403 otherwise | No |
:unauthorized | 401 | No — refresh the token first |
:not_found | 404 | No |
:offset_out_of_range | 400 no_records_found_at_offset_limit | No |
:invalid_request | 400 otherwise | No |
:balance_unavailable | 502 balance_temporarily_unavailable | Yes |
:server_error | 5xx | Yes |
:transport_error | connection failure, timeout | Yes |
:unexpected | anything unclassified | No |
Summary
Functions
Whether OpenFeed metering credit is exhausted for this grant.
Build an error from an HTTP response.
Build an error from a transport-level failure (no HTTP response at all).
Whether this means the grant is dead and no amount of retrying will help.
Build an error that did not come from HTTP at all — a missing key, say.
Whether retrying the same request could plausibly succeed.
Types
@type kind() ::
:grant_revoked
| :credit_exhausted
| :subject_mismatch
| :forbidden
| :unauthorized
| :not_found
| :offset_out_of_range
| :invalid_request
| :balance_unavailable
| :server_error
| :transport_error
| :unexpected
Functions
Whether OpenFeed metering credit is exhausted for this grant.
@spec from_response(pos_integer(), term(), String.t() | nil) :: t()
Build an error from an HTTP response.
The body is expected to be OpenFeed's {"code": ..., "message": ...} shape,
but nothing depends on it being well formed — an HTML error page from a proxy
still classifies by status.
Build an error from a transport-level failure (no HTTP response at all).
Whether this means the grant is dead and no amount of retrying will help.
True only for :grant_revoked. Deliberately not true for
:subject_mismatch, which is also a 403 but indicates a bug rather than a
withdrawn consent.
Build an error that did not come from HTTP at all — a missing key, say.
Whether retrying the same request could plausibly succeed.
True for server and transport errors, and for :balance_unavailable — which
means OpenFeed could not reach the upstream data holder, not that anything is
wrong with your request.