Guides
Syncing and budgets
Reading a connection signs in to the institution, and institutions notice an account that signs in too often. So every connection has a sign-in budget, and the API is shaped to make one sign-in go a long way.
One refresh, one sign-in
POST /v1/connections/{connection_id}/refresh reads accounts and a transaction sync from one sign-in. Use it whenever you want both.
GET /v1/connections/{connection_id}/accounts followed by POST /v1/connections/{connection_id}/sync reads the same things with two sign-ins, seconds apart. That pattern looks like a script to an institution, and at an institution with a small budget the second call is refused.
Cursors
A sync returns what changed since the cursor you send, and a new cursor.
- Send no cursor the first time.
- Write
addedandmodifiedbytransaction_id, and deleteremoved_ids. - Store the returned
cursorwith the connection, and send it next time exactly as it came.
A cursor is opaque: don't read anything into it. A sync can return a transaction you already have, so always upsert by transaction_id rather than insert. History can also arrive over more than one sync, so a first sync may not reach as far back as the next one does.
curl -X POST "https://api.alkirai.com/v1/connections/$CONNECTION_ID/sync?cursor=$CURSOR" \
-H "X-Client-Id: $ALKIRAILINK_CLIENT_ID" \
-H "X-API-Key: $ALKIRAILINK_SECRET"
When partial_failure is set, the read stopped partway. What arrived is real: store it and the cursor, record the refresh as incomplete, and sync again later.
Reading straight after a connect
The connect has just signed in, with the person there. AlkiraiLink keeps that session for 5 minutes waiting for your first refresh or sync, and answers it from there. For 15 minutes after the connect, reads of the connection are answered from that first read without signing in again, whatever cursor you send.
So read a new connection soon after the exchange, and expect the reads in that window to repeat what you already have.
Sign-in budgets
Each institution allows a number of sign-ins per connection in a window of hours, and the connect's own sign-in counts. Read where a connection stands with GET /v1/connections/{connection_id}, which signs in to nothing:
"sign_ins": {
"recent": 1,
"limit": 3,
"window_hours": 6.0,
"last_at": "2026-10-20T07:00:12Z",
"next_at": null
}
- When the budget is spent, the connection is
at_limit, and a read answers429withRetry-After. Nothing is wrong; wait untilsign_ins.next_at. - One sign-in runs per connection at a time. A second read of the same connection while one is running also answers
429. - Budgets differ by institution, and some allow about one sign-in a day. Plan your schedule around the budget you read, never a number you assume.
Never retry a read in a loop. Each try can spend a sign-in. Retry only when error.retryable is true, and wait for error.retry_after when it is set.
Connection states
GET /v1/connections/{connection_id} and GET /v1/connections report what the next sync would meet.
| State | What it means | What to do |
|---|---|---|
active | A sync would sign in now. | Refresh on your schedule. |
at_limit | The sign-in budget is spent until sign_ins.next_at. | Wait. Nothing is wrong. |
stopped | Syncs are refused, without contacting the institution, until something changes. error says what stopped it. | Follow error.user_action: see below. |
When a connection stops
A sign-in that fails, for any reason that isn't retryable, stops the connection rather than trying the institution again. Every read then answers 409 with the same error until the cause is cleared, and nothing more is sent to the institution.
reconnect: the institution no longer accepts the saved details. Ask the person to connect again, then remove the old connection.contact_institution: the institution has locked the account. The person clears it with the institution first, and then reconnects.nonewithconnector_broken: the problem is ours. Keep your usual schedule: the connection restarts once it is fixed, with nobody reconnecting.
Show the person error.display_message when it is set. It is written for them; error.message is for you.
Categories and merchant names
Every transaction a sync or a refresh returns carries a personal_finance_category (PersonalFinanceCategory) and, where a business can be named, a merchant_name. Both are read from the institution's own description with rules written for the region.
primaryis the broad category, such asFOOD_AND_DRINKorINCOME;detailedis the precise one, and is always one ofprimary's own.confidence_levelsays how sure the category is.HIGHmeans the institution called the row a fee or the payee is a known business;MEDIUMmeans the description's words said what it was;LOWis a card purchase that names nothing more, or a row nothing matched (OTHER_OTHER).merchant_nameis empty for transfers, deposits, cheques and anything else that could name a person.
Treat a category as a default. When the person changes one, or your own rules say otherwise, keep their choice: a later sync sends the same row with the same category again.
A refresh schedule
- Refresh each connection no more often than its budget allows. For most apps, once a day is enough.
- Before a refresh, skip any connection that isn't
active: reading the state costs nothing.