Bank connections
A bank connection (bank_connection) links a company in your workspace to its bank, through a consent page hosted by our aggregator. A connection is not a bank account: accounts appear under it once a sync has run. No endpoint on this page emits anything to the regulatory network - neither PPF (Portail Public de Facturation) nor Peppol. Opening a consent session does call our aggregator, which is the whole point of the call.
The constraint to know before you start
There is no bank connection without a browser. A server-to-server integration cannot establish a single one: at some point a human must open the aggregator's consent page, choose their bank and authenticate with it. The API opens that page and gives you its address; it does not walk through it for you.
This is a constraint of the aggregator and the banks, not a Scribee limitation, and there is no way around it.
What Scribee does for you
- Opens the consent session with the aggregator and returns the address for you to have opened.
- Advances
statusonly on durable evidence - an event received from the aggregator or an explicit sync. The browser's return to your site is not evidence: it is a navigation signal, which anyone can replay. - Prevents a replayed call from opening a second session, through
Idempotency-Key. - Never publishes the aggregator's identifiers: they stay internal.
The endpoints
GET/POST /api/v1/workspaces/{workspace_id}/companies/{company_id}/bank_connections- list and openGET /api/v1/bank_connections/{id}- read one connection
As with accounting ledgers (Accounting ledgers), only the list and the creation are addressed per workspace and per company. GET /api/v1/bank_connections/{id} has no workspace_id in the path: the identifier is resolved across every workspace attached to your OAuth client.
A connection carries ten fields, all present in every response: id, company_id, status, origin, bank_name, last_synced_at, consent_url, consent_url_expires_at, created_at and updated_at.
last_synced_at is the later of two dates: the last read of the connection's state from the aggregator, and the last company sync that ran to completion, in full or in part. A failed sync does not stamp it at its end, but the read of the connection's state it starts with may still have advanced it, when that read itself succeeded. It does not move while the connection is disconnected. It is null until either has happened.
status takes one of these five values:
status | What it means |
|---|---|
pending | The consent session is open, nobody has completed it yet |
connected | Durable evidence confirms access to the bank |
disconnected | Access has been revoked; the history already imported is kept |
expired | Authentication with the bank has expired and must be redone |
error | The aggregator reports a problem on this connection |
Step 1: open a consent session
This call runs against your production workspace - there is no sandbox. A token carrying the write scope is required, along with an Idempotency-Key header.
user_email is required: it is the address of the person who will open the consent page. The aggregator refuses to open a session without one, and it may ask that person to validate it during the flow - so send the address of the human you are about to redirect, not a generic mailbox. We cannot derive it: on a partner flow there is no signed-in user on our side, and the workspace member who owns the company is not the one in front of the screen.
redirect_url is optional. It tells Scribee where to send the browser back to you once the session is finished; it must be one of the redirect URIs registered for your OAuth application, otherwise the call is refused.
The aggregator sends the browser to Scribee. That detour is what lets us record the session's outcome on the connection whose id you have just received, before handing you back control. There is nothing for you to do about this step other than know it exists.
curl -X POST https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/companies/YOUR_COMPANY_ID/bank_connections \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Idempotency-Key: 4f1c2a6e-0d1b-4f2a-9a11-8d0e2c3b7a55" \
-H "Content-Type: application/json" \
-d '{
"user_email": "consenting.user@example.com",
"redirect_url": "https://partner.example.com/banking/return"
}'
201 response:
{
"data": {
"id": 501,
"company_id": 34,
"status": "pending",
"origin": "synchronized",
"bank_name": null,
"last_synced_at": null,
"consent_url": "https://connect.example-provider.com/s/abc123",
"consent_url_expires_at": "2026-08-21T11:30:00+02:00",
"created_at": "2026-08-21T11:15:00+02:00",
"updated_at": "2026-08-21T11:15:00+02:00"
}
}
This 201 means a session exists, never that a bank is connected. The connection is pending and stays there until durable evidence arrives.
consent_url is single-use and short-lived. Do not cache it, do not log it, do not re-serve it to another user.
consent_url_expires_at carries the deadline we enforce, and it should be treated as one. Past it the page may still open, but a flow finished afterwards can no longer advance this connection: warn the user or open a new session rather than waiting. The aggregator itself returns no expiry date - we publish ours, not a guess at theirs.
When the answer is a 202
Sometimes the aggregator answers without settling anything: a timeout, an error on its side, or a success carrying no address. We then do not know whether a session was opened, and answering 201 or refusing you outright would both be claims we cannot stand behind.
So you get a 202 carrying the connection, with no consent_url - there is none to give. What to do with it:
- poll the
id. If a session did exist and the person completes it, this connection is the one that advances; - if it is still
pendingpastconsent_url_expires_at, open a new session with a freshIdempotency-Key.
Replaying the same key returns this same response instead of opening a second session.
Resuming a connection: reconnect
A connection goes expired when authentication with the bank runs out, or error when the aggregator reports a problem. Either way, resuming means a new consent session:
curl -X POST https://app.scribee.tech/api/v1/bank_connections/501/reconnect \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Idempotency-Key: 9b2e1f04-7c3a-4d15-b8e6-2a5c7d9e1f30" \
-H "Content-Type: application/json" \
-d '{
"redirect_url": "https://partner.example.com/banking/return"
}'
You get the connection back with a fresh consent_url, to be opened by a human exactly as the first time.
Two differences from creation, and they matter:
statusdoes not move. A new session is an intention, not evidence: the connection stays where the last read left it until an event or a sync says otherwise;- no
user_emailis expected. The aggregator's resume flow identifies the item, not a person.
A connection that never reached the aggregator - left pending with no session ever completing - has nothing to resume: the call is refused with invalid_argument. Open a new connection instead.
Disconnecting a connection: DELETE
curl -X DELETE https://app.scribee.tech/api/v1/bank_connections/501 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
This is not a data-deletion endpoint. status becomes disconnected, the aggregator's access to the bank stops, and everything already imported is retained: accounts, operations, entries. The connection itself stays readable.
A token carrying the destroy scope is enough; write is accepted too.
To resume later, reconnect opens a new session on this same connection.
Step 2: have a human open the page
Send the user to consent_url. There they choose their bank and authenticate. At the end, the aggregator returns the browser to Scribee, which records the session's outcome and then sends it on to your redirect_url if you supplied one.
That return proves nothing. It carries no authority: do not use it to display a success, nor to trigger any processing. Use it only to bring the user back into your interface. What it does guarantee is that the connection whose id you hold is the one the rest of the flow will advance - not that it is connected.
Step 3: poll the connection
This is where you learn what actually happened. A read token is enough.
curl https://app.scribee.tech/api/v1/bank_connections/501 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
status becomes connected only once an aggregator event or a sync has confirmed it. While it reads pending, the connection is not established - whether or not the user has come back to your site.
bank_name carries the bank's name once we have been able to link the connection to it, and is null until that link is made. Two ordinary cases leave it null: the bank chosen is not in our catalogue yet, which is a snapshot of what the aggregator publishes and not a guarantee; or the aggregator has told us nothing about this connection's bank. So expect null including on a connected connection: this field says nothing about the connection's state, status is what carries it.
consent_url is null on this response: it is populated only when a session is opened.
Step 4: list a company's connections
curl "https://app.scribee.tech/api/v1/workspaces/YOUR_WORKSPACE_ID/companies/YOUR_COMPANY_ID/bank_connections?status=connected" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
The list is returned newest first and pages with page and per_page. The status filter accepts one of the five values above; any other value simply matches nothing.
include=bank_accounts adds to each connection the list of accounts that depend on it, each with its whole payload - the same one described in Bank accounts. Without that parameter, the key is absent.
Errors
| Status | code | When |
|---|---|---|
422 | validation_failed | The Idempotency-Key header or the user_email field is missing |
422 | invalid_argument | The user_email supplied is not a valid address, or the redirect_url supplied is not registered for your application |
422 | provider_quota_exceeded | The aggregator refuses to open one more connection for this account |
422 | operation_failed | The aggregator refused the session for a transient reason, or another session is already being opened for the same company - retry |
409 | idempotency_key_reuse | The same Idempotency-Key has already served a different body |
409 | idempotency_request_in_progress | An earlier call carrying this key is still running |
403 | - | Your OAuth client holds no grant on this workspace |
404 | - | The company or the connection is not reachable by your grants |
The two 409s are not retried the same way. idempotency_key_reuse asks for a fresh key - or for the original body, if what you wanted was to replay the stored response. idempotency_request_in_progress stays refused until the key expires: it is a signal to go and check what the first call did, never an invitation to loop.
provider_quota_exceeded names an action on your side: free an existing connection or move to another plan with us. Retrying unchanged will be refused again.