API quickstart
This walkthrough takes you from a brand-new client application to a successful
API call. You’ll register a client, create a sandbox organization, run the
OAuth authorization flow against it, exchange the code for tokens, and create
an invoice. You can do it entirely from a terminal with curl, plus openssl
or Node.js to generate the PKCE values.
The whole walkthrough runs against your sandbox — a non-live organization seeded with demo data — so nothing you do here touches real books. All you need is an AccountFUN account with a confirmed email address. See Sandbox for the full sandbox lifecycle, lockdown guarantees, and limits.
1. Register a client application
Section titled “1. Register a client application”- Sign in to AccountFUN, go to Settings → API & Developers and click New App.
- Set the name to something like “My Integration”.
- Add a redirect URI. For local testing, use
http://localhost:3000/callback. - Select the scopes:
contacts:read,invoices:read,invoices:write,invoices:approve,chart_of_accounts:read,sandbox:reset, andoffline_access. - Save. Copy the client ID and client secret now — the secret is only shown once.
Export them as environment variables for the rest of this guide:
export CLIENT_ID="your_client_id_here"export CLIENT_SECRET="your_client_secret_here"export REDIRECT_URI="http://localhost:3000/callback"2. Create a sandbox
Section titled “2. Create a sandbox”Go back to Settings → API & Developers. The sandbox card sits at the top of the page — click Create sandbox.
AccountFUN creates a second, non-live organization seeded with demo data:
contacts at @example.com addresses, and invoices and bills spanning the
document lifecycle. It appears in your organization switcher badged
(Sandbox), and nothing you do in it can reach a real inbox or move real
money. One sandbox per developer; you can reset it to its fresh seed at any
time.
3. Send the user to authorize
Section titled “3. Send the user to authorize”Every authorize request needs a random state and a
PKCE pair: a secret code_verifier
(43–128 characters) that you keep for step 4, and its code_challenge — the
base64url-encoded SHA-256 of the verifier, without = padding — which goes
in the URL. Generate all three in the same terminal:
export STATE=$(openssl rand -hex 16)export CODE_VERIFIER=$(openssl rand -base64 48 | tr -d '\n=' | tr '+/' '-_')export CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr -d '\n=' | tr '+/' '-_')No openssl? Node.js 16 or newer does the same:
eval "$(node -e 'const c = require("node:crypto");const v = c.randomBytes(32).toString("base64url");const ch = c.createHash("sha256").update(v).digest("base64url");console.log(`export STATE=${c.randomBytes(16).toString("hex")} CODE_VERIFIER=${v} CODE_CHALLENGE=${ch}`);')"Then print the authorize URL:
echo "https://app.accountfun.com/api/v1/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=contacts:read%20invoices:read%20invoices:write%20invoices:approve%20chart_of_accounts:read%20sandbox:reset%20offline_access&state=$STATE&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256"Open the printed URL in a browser where you’re signed in to AccountFUN. You’ll see the consent screen. The organization picker lists your sandbox with a (Sandbox) suffix — pick it deliberately, because the token is bound to whichever organization you choose. Review the scopes. Approving invoices is listed under Only if you tick it: — tick it, because step 7 approves an invoice. Then click Allow.
AccountFUN redirects back to your redirect URI with a code. If nothing is
running on localhost:3000, the browser shows an error page; either way, the
code is in its address bar:
http://localhost:3000/callback?code=AUTHORIZATION_CODE&state=YOUR_STATECopy the code:
export CODE="the_authorization_code"The code works once and expires after 60 seconds, so run step 4 straight away. If it has expired, repeat step 3.
4. Exchange the code for tokens
Section titled “4. Exchange the code for tokens”Send the code_verifier from step 3 with the code. AccountFUN checks that its
SHA-256 matches the code_challenge you sent, and that redirect_uri is
exactly the one used in step 3:
curl -X POST https://app.accountfun.com/api/v1/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=authorization_code \ -d code="$CODE" \ -d redirect_uri="$REDIRECT_URI" \ -d client_id="$CLIENT_ID" \ -d client_secret="$CLIENT_SECRET" \ -d code_verifier="$CODE_VERIFIER"A missing or wrong code_verifier returns 400 invalid_grant; the code stays
usable until it expires, so you can retry with the right verifier.
You’ll receive:
{ "access_token": "ab12cd34...", "token_type": "Bearer", "expires_in": 1800, "scope": "contacts:read invoices:read invoices:write invoices:approve chart_of_accounts:read sandbox:reset offline_access", "organization_id": "00000000-...", "refresh_token": "ef34ab12..."}scope lists what you granted. If invoices:approve is missing, you didn’t
tick approving on the consent screen: run steps 3 and 4 again and tick it.
Save the tokens:
export ACCESS_TOKEN="ab12cd34..."export REFRESH_TOKEN="ef34ab12..."export ORG_ID="00000000-..."5. Read your organization
Section titled “5. Read your organization”Confirm the connection works, read the base currency, and check that you’re talking to the sandbox:
curl https://app.accountfun.com/api/v1/organisation \ -H "Authorization: Bearer $ACCESS_TOKEN"The response includes a sandbox marker:
{ "organization": { "id": "00000000-...", "name": "My Sandbox", "baseCurrency": "USD", "sandbox": true }}6. Find a contact and an income account
Section titled “6. Find a contact and an income account”You need a contact ID and an income account ID to create an invoice. The demo seed has both — fetch them:
# A customer to billcurl "https://app.accountfun.com/api/v1/contacts?type=customer&limit=5" \ -H "Authorization: Bearer $ACCESS_TOKEN"
# An income account for the line itemcurl https://app.accountfun.com/api/v1/chart-of-accounts \ -H "Authorization: Bearer $ACCESS_TOKEN"Pick a contactId from the contacts list and an accountId from an account
where type is Revenue, Sales, or OtherIncome.
export CONTACT_ID="00000000-..."export ACCOUNT_ID="00000000-..."7. Create an invoice
Section titled “7. Create an invoice”curl -X POST https://app.accountfun.com/api/v1/invoices \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d "{ \"contactId\": \"$CONTACT_ID\", \"issueDate\": \"2026-08-12\", \"dueDate\": \"2026-09-11\", \"lines\": [ { \"description\": \"API test invoice\", \"quantity\": \"1\", \"unitAmount\": \"99.00\", \"accountId\": \"$ACCOUNT_ID\" } ] }"A 201 Created response with the new draft invoice means you’re connected and
writing data. The invoice appears as a DRAFT inside the sandbox. You can
approve it over the API to post it straight to the books — this uses the
invoices:approve scope you ticked in step 3. Copy the id from the 201
response and call the approve endpoint:
curl -X POST https://app.accountfun.com/api/v1/invoices/INVOICE_ID/approve \ -H "Authorization: Bearer $ACCESS_TOKEN"Approving records the revenue and, if the line references a tracked item,
updates stock and cost of goods sold automatically. See
Invoices for the full flow — including creating an invoice
already approved in a single call ("status": "APPROVED").
8. Reset the sandbox
Section titled “8. Reset the sandbox”Finish the loop by returning the sandbox to its fresh demo seed:
curl -X POST https://app.accountfun.com/api/v1/sandbox/reset \ -H "Authorization: Bearer $ACCESS_TOKEN"A 200 OK confirms the reset:
{ "organizationId": "00000000-...", "resetAt": "2026-08-31T12:00:00.000Z", "status": "reset-to-fresh-seed"}All data in the sandbox — including the invoice you just created — is wiped
and the demo dataset is reseeded. Your token still works: reset never revokes
grants or tokens, so a follow-up GET /api/v1/invoices lists the fresh demo
invoices. You can reset up to 10 times per hour; see Sandbox
for reset semantics and limits.
9. Refresh the token (later)
Section titled “9. Refresh the token (later)”Access tokens expire after 30 minutes. When yours does, use the refresh token to get a new pair:
curl -X POST https://app.accountfun.com/api/v1/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN" \ -d client_id="$CLIENT_ID" \ -d client_secret="$CLIENT_SECRET"Update your stored refresh token
The response contains a new refresh_token. The old one is now dead —
replace it in your storage immediately. Reusing the old token will invalidate
the entire grant family.
Going live
Section titled “Going live”When your integration is ready for real data, run the authorize flow again
and pick a live organization in the consent picker instead of the sandbox.
Everything else — base URL, endpoints, scopes, token handling — is identical.
The "sandbox" field on GET /api/v1/organisation tells you which kind of
organization a token is bound to.
Where to go next
Section titled “Where to go next”- Sample app — a runnable Node.js or Python integration that does everything above in real code.
- Sandbox — the full sandbox lifecycle, lockdown guarantees, and limits.
- Authentication — the complete OAuth reference, including PKCE and revocation.
- Invoices, Contacts — full endpoint reference.
- Errors & rate limits — build resilient error handling.