Skip to content
AccountFUNDocs

Sample app

The API quickstart walks through the OAuth flow with curl. This page points you at a runnable web app that does the same thing in real code — register a client, send a user through consent, exchange the code for tokens, keep the connection alive with refresh-token rotation, and create a draft invoice.

Two complete, dependency-light samples are available as downloadable starter projects — grab the one for your stack, unzip, and run:

  • Builds the authorize URL with a random state and a PKCE code_challenge.
  • Verifies state on the callback (CSRF defense) and exchanges the code for tokens.
  • Stores tokens server-side and refreshes them before they expire.
  • Serializes refreshes per session so two concurrent requests never rotate the same refresh token twice (AccountFUN invalidates that token’s family — the rotation chain from one authorization — on reuse).
  • Calls the resource API to read the organization, list customers, list income accounts, and create a draft invoice.
  • Handles a dead grant (expired refresh token, revoked family) by re-prompting authorization.
  1. Have an AccountFUN organization with at least one active customer and one active INCOME account. The sample creates an invoice against them.
  2. Register a client under Settings → API & Developers → New App with the scopes org:read, contacts:read, invoices:read, invoices:write, chart_of_accounts:read, and offline_access. See Authentication for the details.
  3. Add the sample’s callback as a redirect URI on that client — http://localhost:3000/callback for Node, http://localhost:5000/callback for Python. http://localhost is allowed for development.

Pick a language, install dependencies, and fill in your client credentials. The app prints the exact callback URL you need to have registered.

Terminal
cd node-oauth-demo
npm install
cp .env.example .env # then add your CLIENT_ID / CLIENT_SECRET
npm start # → http://localhost:3000

Requires Node.js 18.17 or newer (uses the global fetch).

Open the printed URL, click Connect to AccountFUN, approve the consent screen, and you’ll land on a small dashboard. Pick a customer and an income account and click Create draft invoice — a new DRAFT invoice appears in AccountFUN.

The four snippets below are the heart of each sample. They’re excerpted from the downloadable projects above, which also handle the HTML UI, the logout/revoke path, and error rendering.

Generate a random state (CSRF defense) and a PKCE verifier/challenge, then redirect to the authorize endpoint.

const { verifier, challenge } = makePkce();
const state = crypto.randomBytes(16).toString("hex");
req.session.oauth = { verifier, state };
const params = new URLSearchParams({
response_type: "code",
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: SCOPES,
state,
code_challenge: challenge,
code_challenge_method: "S256",
});
res.redirect(`${ACCOUNTFUN_API_URL}/api/v1/oauth/authorize?${params}`);

On the callback, verify state, then POST the code (plus the PKCE verifier) to the token endpoint.

// Never exchange a code without confirming state came back unchanged.
if (state !== oauth.state) return res.status(400).send("Invalid state");
const tok = await postToken({
grant_type: "authorization_code",
code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code_verifier: oauth.verifier, // ties back to the PKCE challenge
});
saveTokens(req.session, tok);

Access tokens expire after 30 minutes. With offline_access you also received a rotating refresh token. The old refresh token dies the moment you use it — store the new one immediately, and never let two requests rotate the same token at once.

// One in-flight refresh per session — a second concurrent rotation would
// invalidate the entire token family.
const tok = await postToken({
grant_type: "refresh_token",
refresh_token: session.tokens.refresh_token,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
});
saveTokens(session, tok); // replaces the now-dead refresh token

Both samples refresh proactively before the access token expires, and again once on a 401 invalid_token to recover from a revoked token. If the refresh itself fails with invalid_grant, the grant is dead and the user is sent back to authorize.

Once you have a valid access token, call the resource API. Send money and quantities as strings, and reference a customer contactId and an INCOME accountId. The samples ask you to choose issue and due dates as calendar days; they send those YYYY-MM-DD values directly, without converting through a time or timezone.

// Read date-only strings from <input type="date">; do not convert to a time.
const res = await apiRequest(req, "POST", "/api/v1/invoices", {
body: {
contactId,
issueDate, // "YYYY-MM-DD"
dueDate, // "YYYY-MM-DD"
lines: [
{ description, quantity: "1", unitAmount: "50.00", accountId },
],
},
});
const { invoice } = await res.json(); // 201 Created

The invoice is created in DRAFT. Approve it inside AccountFUN to post it to your books.

The samples are intentionally minimal. Before shipping a real integration:

  • Store tokens encrypted at rest in a durable store (database, secret manager) — not in the in-memory session these demos use.
  • Run behind HTTPS and use HTTPS redirect URIs (http://localhost is development-only).
  • Rotate the client_secret if it leaks, and revoke tokens via the revoke endpoint when a user disconnects.
  • Handle 429 with Retry-After and apply exponential backoff on 5xx.