Skip to main content

Bank consents

6 operations under /v1/bank-consents (GET POST): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.

16 min read
View MarkdownEdit on GitHub

Base URL: https://api.codespar.dev

Every operation below requires a Bearer token. See Authentication.

GET /v1/bank-consents

GEThttps://api.codespar.dev/v1/bank-consents

List Open Finance bank consents

The organization's Open Finance consents, most recent first.

status filters by one of the five states of the cycle: pending (the consent exists and the person has not yet authorised it at their bank), authorised, revoked, expired and consumed. Absent, all of them come back.

Query parameters

NameTypeRequiredDescription
limitintegerno1 to 200; the default is 50.
status"pending" | "authorised" | "revoked" | "expired" | "consumed"no—

Responses

StatusBodyDescription
200objectOK
400objectThe query did not match the schema.

Response 200

FieldTypeRequiredDescription
bank_consentsarray of objectyes—
Example request
curl -X GET https://api.codespar.dev/v1/bank-consents \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/bank-consents HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/bank-consents",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/bank-consents", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/bank-consents");
Example response 200
application/json
{
  "bank_consents": [
    {
      "id": "obj_0000000000000000",
      "bank_id": "bank_0000000000000000",
      "consumer_id": "csm_0000000000000000",
      "wallet_id": "wlt_0000000000000000",
      "status": "pending",
      "scopes": [
        "string"
      ],
      "bank_consent_id": "bankconsent_0000000000000000",
      "authorisation_url": "https://example.com/hook",
      "token_expires_at": "2026-01-15T12:00:00.000Z",
      "created_at": "2026-01-15T12:00:00.000Z",
      "authorized_at": "2026-01-15T12:00:00.000Z",
      "expires_at": "2026-01-15T12:00:00.000Z",
      "revoked_at": "2026-01-15T12:00:00.000Z",
      "consumed_at": "2026-01-15T12:00:00.000Z",
      "metadata": {}
    }
  ]
}

POST /v1/bank-consents

POSThttps://api.codespar.dev/v1/bank-consents

Initiate a bank consent

Opens a consent at the bank and persists it as pending. POST /v1/ofb/consents is the legacy spelling of this same operation, kept for two releases (ent#979); new integrations use this path.

THE BANK IS CALLED BEFORE THE ROW IS WRITTEN, and that ordering is visible in the failure modes below: every refusal that arrives after the bank call leaves a bank-side consent with no row of ours pointing at it. Retrying opens another one.

One open consent per consumer per bank per organization. A sibling project in your organization holding an open consent for this consumer at this bank is refused, and the refusal is not something a retry clears.

On 201 the consent is pending, and authorisation_url, bank_consent_id and expires_at are all set: the adapter's result declares them non-nullable and the INSERT writes all three. Send the consumer to authorisation_url, then post the code they come back with to the callback operation.

Request body

FieldTypeRequiredDescription
bank_idstringyes—
consumer_idstringyes—
redirect_urlstring (uri)yesWhere the bank returns the consumer after authorisation. You compose it and you host it.
scopesarray of stringyes—
wallet_idstringnoBinds the consent to a wallet, so the statements it ingests carry that wallet.

Responses

StatusBodyDescription
201objectCreated. The consent is pending and authorisation_url is set.
400objectThe body did not match the schema. details.issues carries the Zod issues.
409objectbank_consent_held_elsewhere when another project in this organization holds an open (pending or authorised) consent for this consumer at this bank. details.remediation says what to do and details.retriable is false: one project's bank grant is never shared with another, so re-sending this request unchanged will not clear it. consent_active_for_consumer when YOUR OWN project already holds one that is still inside its validity window. Revoke it first. One past its window does not refuse: it is moved to expired as the new consent is created. db_error when the consent could not be persisted for any other reason. The cause is in our logs under the request id and is deliberately not in this body, because the driver's own message names another project's key values.
422objectofb_not_live: this is a LIVE project and the deployment has no real Open Finance adapter, only the stub, which serves test projects. TERMINAL for this project: use a test project. Nothing was written and no bank was contacted.
500objectconsent_disappeared: the row was written and could not be read back. Do not retry blind; both the row and the bank-side consent may exist.
502objectThe bank refused the request or could not be reached, before anything was written. The message is the adapter's own.
503objectconsent_lock_timeout: another request is issuing a consent for this consumer at this bank and the wait ended without an answer, so NOTHING WAS DECIDED. We do not know whether a sibling holds the grant, and saying so is why this is not the 409. details.retriable is true. No consent row was created; a bank-side consent was, and is orphaned. ofb_adapter_unavailable: the Open Finance adapter this deployment selects cannot serve (details.reason: missing_credentials, not_implemented or unknown_mode). Ours to fix, not yours; nothing was written and no bank was contacted.

Response 201

FieldTypeRequiredDescription
authorisation_urlstring,nullyesWhere the consumer authorises the consent. This is the URL to send them to.
authorized_atstring,null (date-time)yesSpelled with a z, unlike authorised in status. Both spellings are column names, not typos.
bank_consent_idstring,nullyesThe bank's own identifier for the consent. Null until the bank issues one.
bank_idstringyesLowercase kebab-case, for example itau or bb.
consumed_atstring,null (date-time)yes—
consumer_idstringyes—
created_atstring (date-time)yes—
expires_atstring,null (date-time)yesMirrors the bank-side consent expiry, as the bank reported it at initiation.
idstringyes—
metadataobjectyes—
revoked_atstring,null (date-time)yes—
scopesarray of stringyes—
status"pending" | "authorised" | "revoked" | "expired" | "consumed"yes—
token_expires_atstring,null (date-time)yes—
wallet_idstring,nullyesSet when the consent was bound to a wallet at initiation; null when it was not.
Example request
curl -X POST https://api.codespar.dev/v1/bank-consents \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "bank_id": "bank_0000000000000000",
       "consumer_id": "csm_0000000000000000",
       "scopes": [
         "string"
       ],
       "redirect_url": "https://example.com/hook",
       "wallet_id": "wlt_0000000000000000"
     }'
POST /v1/bank-consents HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "bank_id": "bank_0000000000000000",
  "consumer_id": "csm_0000000000000000",
  "scopes": [
    "string"
  ],
  "redirect_url": "https://example.com/hook",
  "wallet_id": "wlt_0000000000000000"
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/bank-consents",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "bank_id": "bank_0000000000000000",
      "consumer_id": "csm_0000000000000000",
      "scopes": [
        "string"
      ],
      "redirect_url": "https://example.com/hook",
      "wallet_id": "wlt_0000000000000000"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/bank-consents", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "bank_id": "bank_0000000000000000",
    "consumer_id": "csm_0000000000000000",
    "scopes": [
      "string"
    ],
    "redirect_url": "https://example.com/hook",
    "wallet_id": "wlt_0000000000000000"
  }),
});

const data = await res.json();
const r = await cs.api.response("post", "/v1/bank-consents", {
  body: {
    bank_id: "bank_0000000000000000",
    consumer_id: "csm_0000000000000000",
    scopes: [
      "string"
    ],
    redirect_url: "https://example.com/hook",
    wallet_id: "wlt_0000000000000000"
  }
});
// r.status is one of the documented statuses (200, 422),
// each with its own body shape in r.data; nothing here throws on 422.
if (r.ok) {
  console.log(r.data);
}
Example response 201
application/json
{
  "id": "obj_0000000000000000",
  "bank_id": "bank_0000000000000000",
  "consumer_id": "csm_0000000000000000",
  "wallet_id": "wlt_0000000000000000",
  "status": "pending",
  "scopes": [
    "string"
  ],
  "bank_consent_id": "bankconsent_0000000000000000",
  "authorisation_url": "https://example.com/hook",
  "token_expires_at": "2026-01-15T12:00:00.000Z",
  "created_at": "2026-01-15T12:00:00.000Z",
  "authorized_at": "2026-01-15T12:00:00.000Z",
  "expires_at": "2026-01-15T12:00:00.000Z",
  "revoked_at": "2026-01-15T12:00:00.000Z",
  "consumed_at": "2026-01-15T12:00:00.000Z",
  "metadata": {}
}

GET /v1/bank-consents/{id}

GEThttps://api.codespar.dev/v1/bank-consents/{id}

Read one bank consent

The body is the consent projection: the same shape POST /v1/bank-consents returns on create, and the same one the callback and revoke transitions return. The refresh-statement operation in this family is the exception, answering with a run summary rather than a consent.

Resolution is scoped to the organization AND the project the credential authenticates for. A consent held by a SIBLING project in the same organization answers 404 here, indistinguishable from one that never existed. That is deliberate rather than incidental: one project's upstream bank grant is never readable from another.

No token material leaves. The row carries references to the bank's access and refresh tokens; they are dropped before the body is built, and token_expires_at is the only thing about the bank token that is returned.

Requires the consents:read scope.

Path parameters

NameTypeRequiredDescription
idstringyesofb_-prefixed consent id.

Responses

StatusBodyDescription
200objectOK
404objectNot Found. No consent with this id in the caller's project. A consent belonging to another project in the same organization answers here too.

Response 200

FieldTypeRequiredDescription
authorisation_urlstring,nullyesThe URL the consumer opens to authorise the consent at their bank.
authorized_atstring,null (date-time)yesAmerican spelling here, unlike status: "authorised"; both mirror their own column names. Stamped in the same statement as the transition into authorised.
bank_consent_idstring,nullyesThe bank's own identifier for the consent, as returned when it was opened.
bank_idstringyesLowercase kebab-case, as supplied when the consent was opened: itau, bradesco, bb, inter.
consumed_atstring,null (date-time)yes—
consumer_idstringyesThe consumer, in the caller's own namespace.
created_atstring (date-time)yes—
expires_atstring,null (date-time)yesMirror of the bank-side consent expiry, stamped when the consent is opened.
idstringyesofb_-prefixed. The id every other operation in this family takes.
metadataobjectyes—
revoked_atstring,null (date-time)yes—
scopesarray of stringyesThe OFB scopes the consent was opened for.
status"pending" | "authorised" | "revoked" | "expired" | "consumed"yesauthorised carries the British spelling, which is the column's own and not a typo. revoked, expired and consumed are terminal: no transition leaves them. Read "is this consent usable now" from status being authorised, never from revoked_at being null.
token_expires_atstring,null (date-time)yesExpiry of the bank access token. The token references themselves are never returned by this API.
wallet_idstring,nullyesThe wallet named on the create request, or null. Written once at creation and never rewritten: neither state transition on this consent touches the column. It is a label, not routing. A statement refresh copies this value onto every event it ingests, and nothing downstream reads it: reconciliation resolves a wallet from the funding source behind an event's connection_id, and the events this family publishes carry no such key. Binding a consent to a wallet therefore does not, on its own, put the ingested statement into that wallet's reconciliation.
Example request
curl -X GET https://api.codespar.dev/v1/bank-consents/{id} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/bank-consents/{id} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/bank-consents/{id}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/bank-consents/{id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/bank-consents/{id}", {
  path: {
    id: "bankconsent_0000000000000000"
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "bank_id": "bank_0000000000000000",
  "consumer_id": "csm_0000000000000000",
  "wallet_id": "wlt_0000000000000000",
  "status": "pending",
  "scopes": [
    "string"
  ],
  "bank_consent_id": "bankconsent_0000000000000000",
  "authorisation_url": "https://example.com/hook",
  "token_expires_at": "2026-01-15T12:00:00.000Z",
  "created_at": "2026-01-15T12:00:00.000Z",
  "authorized_at": "2026-01-15T12:00:00.000Z",
  "expires_at": "2026-01-15T12:00:00.000Z",
  "revoked_at": "2026-01-15T12:00:00.000Z",
  "consumed_at": "2026-01-15T12:00:00.000Z",
  "metadata": {}
}

POST /v1/bank-consents/{id}/callback

POSThttps://api.codespar.dev/v1/bank-consents/{id}/callback

Finalise a consent after the consumer authorises it

Exchanges the bank's authorisation code for tokens and moves the consent to authorised. POST /v1/ofb/consents/{id}/callback is the legacy spelling of this same operation (ent#979).

Only a pending consent can be finalised. A consent that is already authorised answers 409, not a second exchange, so replaying a callback does not mint a second token pair.

The token pointers and the authorised event are one transaction. On success the response is the re-read consent, without token material.

Path parameters

NameTypeRequiredDescription
idstringyes—

Request body

FieldTypeRequiredDescription
auth_codestringyesThe code the bank put on the redirect.

Responses

StatusBodyDescription
200objectOK
400objectThe body did not match the schema. details.issues carries the Zod issues.
404objectNo such consent for this credential's organization AND project. A consent created by a sibling project in the same organization answers the same way: the resolver requires both predicates on purpose, because resolving on organization alone let one project read, transition and refresh another project's consent.
409objectillegal_transition: the consent's current status does not permit authorisation. details.from and details.to carry the pair. consent_expired: the consent is pending and its validity window (details.expires_at) has elapsed. TERMINAL: the grant can no longer be authorised. The consent is moved to expired by this same call, which frees this project's one open consent for the consumer at this bank: initiate a new consent. details.status is the consent's status after the call, which is not expired when a concurrent transition won; if it is still pending, revoke the consent before initiating another. Repeating the callback answers consent_expired again. An expired consent cannot be revoked (illegal_transition).
422objectofb_not_live: this is a LIVE project and the deployment has no real Open Finance adapter, only the stub, which serves test projects. TERMINAL for this project: use a test project. Nothing was written and no bank was contacted.
500objectmissing_bank_consent_id: the stored consent carries no bank-side id, so there is nothing to finalise. Ours to fix, not yours; quote the request id.
502objectThe bank refused the code or could not be reached. What this handler wrote is nothing: the UPDATE runs only after the exchange returns, so the consent is still pending and no token pointer was stored. Whether the code itself is still usable is the BANK's state and not ours to report, so this answer does not claim either way. The message is the adapter's own, and empty when what the adapter threw was not an Error.
503objectofb_adapter_unavailable: the Open Finance adapter this deployment selects cannot serve (details.reason: missing_credentials, not_implemented or unknown_mode). Ours to fix, not yours; nothing was written and no bank was contacted.

Response 200

FieldTypeRequiredDescription
authorisation_urlstring,nullyesWhere the consumer authorises the consent. This is the URL to send them to.
authorized_atstring,null (date-time)yesSpelled with a z, unlike authorised in status. Both spellings are column names, not typos.
bank_consent_idstring,nullyesThe bank's own identifier for the consent. Null until the bank issues one.
bank_idstringyesLowercase kebab-case, for example itau or bb.
consumed_atstring,null (date-time)yes—
consumer_idstringyes—
created_atstring (date-time)yes—
expires_atstring,null (date-time)yesMirrors the bank-side consent expiry, as the bank reported it at initiation.
idstringyes—
metadataobjectyes—
revoked_atstring,null (date-time)yes—
scopesarray of stringyes—
status"pending" | "authorised" | "revoked" | "expired" | "consumed"yes—
token_expires_atstring,null (date-time)yes—
wallet_idstring,nullyesSet when the consent was bound to a wallet at initiation; null when it was not.
Example request
curl -X POST https://api.codespar.dev/v1/bank-consents/{id}/callback \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "auth_code": "string"
     }'
POST /v1/bank-consents/{id}/callback HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "auth_code": "string"
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/bank-consents/{id}/callback",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "auth_code": "string"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/bank-consents/{id}/callback", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "auth_code": "string"
  }),
});

const data = await res.json();
const r = await cs.api.response("post", "/v1/bank-consents/{id}/callback", {
  path: {
    id: "bankconsent_0000000000000000"
  },
  body: {
    auth_code: "string"
  }
});
// r.status is one of the documented statuses (200, 422),
// each with its own body shape in r.data; nothing here throws on 422.
if (r.ok) {
  console.log(r.data);
}
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "bank_id": "bank_0000000000000000",
  "consumer_id": "csm_0000000000000000",
  "wallet_id": "wlt_0000000000000000",
  "status": "pending",
  "scopes": [
    "string"
  ],
  "bank_consent_id": "bankconsent_0000000000000000",
  "authorisation_url": "https://example.com/hook",
  "token_expires_at": "2026-01-15T12:00:00.000Z",
  "created_at": "2026-01-15T12:00:00.000Z",
  "authorized_at": "2026-01-15T12:00:00.000Z",
  "expires_at": "2026-01-15T12:00:00.000Z",
  "revoked_at": "2026-01-15T12:00:00.000Z",
  "consumed_at": "2026-01-15T12:00:00.000Z",
  "metadata": {}
}

POST /v1/bank-consents/{id}/refresh-statement

POSThttps://api.codespar.dev/v1/bank-consents/{id}/refresh-statement

Fetch the bank statement and ingest it

Pulls transactions from the bank under an authorised consent and ingests them as events the reconciliation engine picks up. POST /v1/ofb/consents/{id}/refresh-statement is the legacy spelling of this same operation (ent#979).

since and until bound what is fetched. Both are optional; the adapter decides the window when they are absent.

SAFE TO CALL TWICE. Each entry is ingested under a dedup key of (source, the bank's own event id, project), and an entry that key has already seen is counted in duplicates rather than credited again. That is what makes imported and duplicates mean what they say.

The answer is 202 rather than 200 because the statement is fetched and ingested synchronously but RECONCILIATION IS NOT: matching these events against the ledger happens on a later cycle.

Path parameters

NameTypeRequiredDescription
idstringyes—

Request body

FieldTypeRequiredDescription
sincestring (date-time)no—
untilstring (date-time)no—

Responses

StatusBodyDescription
202objectAccepted. The statement was fetched and ingested; reconciliation happens after.
400objectThe body did not match the schema: since and until must be RFC 3339 timestamps carrying an offset. details.issues carries the Zod issues.
404objectNo such consent for this credential's organization AND project. A consent created by a sibling project in the same organization answers the same way: the resolver requires both predicates on purpose, because resolving on organization alone let one project read, transition and refresh another project's consent.
409objectconsent_not_authorised: the consent is not in authorised. details.status carries what it is. consent_expired: the consent's validity window (details.expires_at) has elapsed. The consent is moved to expired by this same call and nothing is fetched. TERMINAL: initiate a new consent. consent_token_expired: the consent is still valid but its access token (details.token_expires_at) has expired. NOT terminal: the consent stays authorised, because the grant is alive at the bank and the token is renewable. Nothing was fetched; do not initiate a new consent.
422objectofb_not_live: this is a LIVE project and the deployment has no real Open Finance adapter, only the stub, which serves test projects. TERMINAL for this project: use a test project. Nothing was written and no bank was contacted.
500objectconsent_missing_tokens: the consent says authorised but is missing something the fetch needs. TWO conditions raise it and the code does not distinguish them: no stored access-token pointer, OR no bank-side consent id. The second is not a token, so read the code as this authorised consent is not usable rather than as a statement about credentials. Ours to fix, not yours; quote the request id.
502objectThe bank refused or could not be reached. Partial progress is KEPT: each entry is committed as it is ingested, so entries taken before the failure stay ingested and a retry re-imports only what is missing. The message is the adapter's own unless the failure came from our own database, in which case it is replaced by a fixed sentence rather than naming our tables.
503objectofb_adapter_unavailable: the Open Finance adapter this deployment selects cannot serve (details.reason: missing_credentials, not_implemented or unknown_mode). Ours to fix, not yours; nothing was written and no bank was contacted.

Response 202

FieldTypeRequiredDescription
consent_idstringyes—
duplicatesintegeryesEntries a previous call had already ingested, recognised by the dedup key and not credited a second time.
importedintegeryesEntries ingested as new events by this call.
next_recon_cycle_within_secondsintegeryesA constant hint about how often reconciliation runs. Not a queue estimate and not a promise about this run.
run_idstringyesIdentifies this refresh in the consent's event log, on the started, completed and failed events alike. Quote it in support.
Example request
curl -X POST https://api.codespar.dev/v1/bank-consents/{id}/refresh-statement \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "since": "2026-01-15T12:00:00.000Z",
       "until": "2026-01-15T12:00:00.000Z"
     }'
POST /v1/bank-consents/{id}/refresh-statement HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "since": "2026-01-15T12:00:00.000Z",
  "until": "2026-01-15T12:00:00.000Z"
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/bank-consents/{id}/refresh-statement",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "since": "2026-01-15T12:00:00.000Z",
      "until": "2026-01-15T12:00:00.000Z"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/bank-consents/{id}/refresh-statement", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "since": "2026-01-15T12:00:00.000Z",
    "until": "2026-01-15T12:00:00.000Z"
  }),
});

const data = await res.json();
const r = await cs.api.response("post", "/v1/bank-consents/{id}/refresh-statement", {
  path: {
    id: "bankconsent_0000000000000000"
  },
  body: {
    since: "2026-01-15T12:00:00.000Z",
    until: "2026-01-15T12:00:00.000Z"
  }
});
// r.status is one of the documented statuses (200, 422),
// each with its own body shape in r.data; nothing here throws on 422.
if (r.ok) {
  console.log(r.data);
}
Example response 202
application/json
{
  "consent_id": "consent_0000000000000000",
  "run_id": "run_0000000000000000",
  "imported": 0,
  "duplicates": 0,
  "next_recon_cycle_within_seconds": 0
}

POST /v1/bank-consents/{id}/revoke

POSThttps://api.codespar.dev/v1/bank-consents/{id}/revoke

Revoke a bank consent

Moves a pending or authorised consent to revoked. POST /v1/ofb/consents/{id}/revoke is the legacy spelling of this same operation (ent#979).

THIS STOPS US, NOT THE BANK. The handler calls no bank adapter: it flips our row and appends a revoked event, so what ends is our use of the consent. If the grant must also end at the bank, revoke it in the bank's own channel as well.

Revoking frees the (organization, consumer, bank) slot, so a new consent can be initiated afterwards. revoked is terminal: a second revoke answers 409.

Path parameters

NameTypeRequiredDescription
idstringyes—

Request body

FieldTypeRequiredDescription
reasonstringnoRecorded on the revoked event.

Responses

StatusBodyDescription
200objectOK
400objectThe body did not match the schema. details.issues carries the Zod issues.
404objectNo such consent for this credential's organization AND project. A consent created by a sibling project in the same organization answers the same way: the resolver requires both predicates on purpose, because resolving on organization alone let one project read, transition and refresh another project's consent.
409objectThe consent's current status does not permit this transition. details.from and details.to carry the pair. pending may become authorised, revoked or expired; authorised may become revoked, expired or consumed; revoked, expired and consumed are terminal and permit nothing.

Response 200

FieldTypeRequiredDescription
authorisation_urlstring,nullyesWhere the consumer authorises the consent. This is the URL to send them to.
authorized_atstring,null (date-time)yesSpelled with a z, unlike authorised in status. Both spellings are column names, not typos.
bank_consent_idstring,nullyesThe bank's own identifier for the consent. Null until the bank issues one.
bank_idstringyesLowercase kebab-case, for example itau or bb.
consumed_atstring,null (date-time)yes—
consumer_idstringyes—
created_atstring (date-time)yes—
expires_atstring,null (date-time)yesMirrors the bank-side consent expiry, as the bank reported it at initiation.
idstringyes—
metadataobjectyes—
revoked_atstring,null (date-time)yes—
scopesarray of stringyes—
status"pending" | "authorised" | "revoked" | "expired" | "consumed"yes—
token_expires_atstring,null (date-time)yes—
wallet_idstring,nullyesSet when the consent was bound to a wallet at initiation; null when it was not.
Example request
curl -X POST https://api.codespar.dev/v1/bank-consents/{id}/revoke \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "reason": "string"
     }'
POST /v1/bank-consents/{id}/revoke HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "reason": "string"
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/bank-consents/{id}/revoke",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "reason": "string"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/bank-consents/{id}/revoke", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "reason": "string"
  }),
});

const data = await res.json();
const result = await cs.api.post("/v1/bank-consents/{id}/revoke", {
  path: {
    id: "bankconsent_0000000000000000"
  },
  body: {
    reason: "string"
  }
});
Example response 200
application/json
{
  "id": "obj_0000000000000000",
  "bank_id": "bank_0000000000000000",
  "consumer_id": "csm_0000000000000000",
  "wallet_id": "wlt_0000000000000000",
  "status": "pending",
  "scopes": [
    "string"
  ],
  "bank_consent_id": "bankconsent_0000000000000000",
  "authorisation_url": "https://example.com/hook",
  "token_expires_at": "2026-01-15T12:00:00.000Z",
  "created_at": "2026-01-15T12:00:00.000Z",
  "authorized_at": "2026-01-15T12:00:00.000Z",
  "expires_at": "2026-01-15T12:00:00.000Z",
  "revoked_at": "2026-01-15T12:00:00.000Z",
  "consumed_at": "2026-01-15T12:00:00.000Z",
  "metadata": {}
}
Bank consents | CodeSpar