Confidential client
A server-side web app or service that can protect a secret.
Uses a client ID, a client secret, and PKCE.
Accrawl API
Use Connect with Accrawl to let a user approve access to specific connections and data. Your app receives a scoped token; it never receives the user’s Accrawl password or financial-institution credentials.
Register your app
Organisation administrators register and manage their organisation’s apps in the Accrawl dashboard. Registration returns a client ID and, for a confidential app, a client secret that is shown once.
Register or manage appsA server-side web app or service that can protect a secret.
Uses a client ID, a client secret, and PKCE.
A native or installed app that cannot protect a secret.
Uses a client ID and PKCE. Public clients do not receive a client secret.
Never put a confidential client secret in browser or mobile code.
Accrawl API
These steps assume a confidential client with a backend. A public client with no backend keeps the verifier and state in secure app storage, and calls the token endpoint directly without a client secret.
Create a fresh PKCE verifier, its S256 challenge, and a random state value. Store the verifier and state with the authorisation attempt.
Send the user to Accrawl’s authorisation page. They sign in, review the requested access, choose which connections to share, and approve or deny.
At your callback URL, fail the request if the response carries an error parameter, or if the returned state doesn’t match the value you stored. Only then exchange the authorization code—along with the PKCE verifier you generated for this request—for tokens. The authorization code can be redeemed just once.
Store both tokens encrypted at rest. Send the access token as a Bearer credential, and replace both stored tokens after every successful refresh.
Generate state and PKCE values for every attempt. The example uses Node.js built-in modules. Adapt session, request, and response to your web framework.
import { createHash, randomBytes } from 'node:crypto';
const baseUrl = 'https://accrawl.com';
const clientId = process.env.ACCRAWL_CLIENT_ID;
const redirectUri = 'https://your-app.example/callback';
if (!clientId) throw new Error('ACCRAWL_CLIENT_ID is not configured');
const verifier = randomBytes(32).toString('base64url');
const challenge = createHash('sha256')
.update(verifier)
.digest('base64url');
const state = randomBytes(16).toString('base64url');
session.accrawlOAuth = { state, verifier };
const authorizeUrl = new URL('/oauth/authorize', baseUrl);
authorizeUrl.search = new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: redirectUri,
scope: 'read:data',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
}).toString();
response.redirect(authorizeUrl.toString());Exchange the code from a trusted backend, or directly from a public client that has no backend. Omit client_secret for a public client.
const callbackUrl = new URL(request.url);
const pending = session.accrawlOAuth;
const returnedState = callbackUrl.searchParams.get('state');
const code = callbackUrl.searchParams.get('code');
const oauthError = callbackUrl.searchParams.get('error');
if (oauthError) throw new Error('Accrawl authorisation was not completed');
if (!pending || !code || returnedState !== pending.state) {
throw new Error('Invalid Accrawl OAuth callback');
}
delete session.accrawlOAuth;
const form = new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
client_id: clientId,
code_verifier: pending.verifier,
});
if (process.env.ACCRAWL_CLIENT_SECRET) {
form.set('client_secret', process.env.ACCRAWL_CLIENT_SECRET);
}
const tokenResponse = await fetch('https://accrawl.com/oauth/token', {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: form,
});
if (!tokenResponse.ok) {
throw new Error(`Accrawl token exchange failed (${tokenResponse.status})`);
}
const tokens = await tokenResponse.json();Data API
Send the access token as a Bearer credential. Start by listing the connections the user approved. Each one names its institution, so you never have to show an internal identifier. Then use a connection ID for the connection-scoped routes.
const apiResponse = await fetch(
'https://accrawl.com/api/v1/connections',
{ headers: { authorization: `Bearer ${tokens.access_token}` } },
);
if (!apiResponse.ok) {
throw new Error(`Accrawl API request failed (${apiResponse.status})`);
}
const connections = await apiResponse.json();
The API reads account data Accrawl has already collected. Every route is a GET. Your app cannot start a refresh, watch one in progress, or submit a one-time passcode — those stay with the person whose accounts these are, and happen only in Accrawl itself. Connections refresh on their own schedule, so check how current the data is before you show it: lastSyncedAt on a connection, asOf on a balance. Show your users that date rather than implying the data is live.
| Scope | Allows your app to |
|---|---|
read:data | Read approved connections, accounts, balances, transactions, and holdings. |
read:data is the only scope, because reading is all the API does. Every token is also restricted to the connections the user approved.
| Method | Route | Purpose |
|---|---|---|
| GET | /api/v1/connections | List the connections the user approved, each with its institution’s name, type, and logo for display. |
| GET | /api/v1/connections/:id/accounts | List accounts and balances for an approved connection. |
| GET | /api/v1/connections/:id/transactions | List transactions, optionally filtered by booking date. |
| GET | /api/v1/connections/:id/transactions/sync | Read incremental transaction changes since a cursor. Omit the cursor on the first call. |
| GET | /api/v1/connections/:id/holdings | List holdings and their referenced securities. |
Token lifecycle
A refresh returns a new access token and a new single-use refresh token without extending the user’s consent. Send the current refresh token once, then replace both stored tokens atomically before using either replacement.
When the user disconnects your app, revoke the current refresh token. This revokes the full grant, including its access tokens.
Implement saveTokensAtomically as one database transaction that replaces both stored tokens.
function clientForm(fields) {
const form = new URLSearchParams({ ...fields, client_id: clientId });
if (process.env.ACCRAWL_CLIENT_SECRET) {
form.set('client_secret', process.env.ACCRAWL_CLIENT_SECRET);
}
return form;
}
const refreshResponse = await fetch('https://accrawl.com/oauth/token', {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: clientForm({
grant_type: 'refresh_token',
refresh_token: storedRefreshToken,
}),
});
if (!refreshResponse.ok) throw new Error('Accrawl token refresh failed');
const replacementTokens = await refreshResponse.json();
await saveTokensAtomically(replacementTokens);const revokeResponse = await fetch('https://accrawl.com/oauth/revoke', {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: clientForm({
token: storedRefreshToken,
token_type_hint: 'refresh_token',
}),
});
if (!revokeResponse.ok) throw new Error('Accrawl token revocation failed');Access and refresh tokens expire together at the end of the same authorisation window, about 90 days after the user’s approval. Refreshing rotates both tokens within that window; it does not extend the user’s consent, so the user must reconnect when the window ends.
Security requirements
The callback can contain error=access_denied when the user cancels or denies access. Token endpoints return standard OAuth error fields. Show the user a safe recovery message, and keep credentials and raw error responses out of the interface.