Skip to content
AccountFUNDocs

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. Sign in to AccountFUN, go to Settings → API & Developers and click New App.
  2. Set the name to something like “My Integration”.
  3. Add a redirect URI. For local testing, use http://localhost:3000/callback.
  4. Select the scopes: contacts:read, invoices:read, invoices:write, invoices:approve, chart_of_accounts:read, sandbox:reset, and offline_access.
  5. 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:

Terminal window
export CLIENT_ID="your_client_id_here"
export CLIENT_SECRET="your_client_secret_here"
export REDIRECT_URI="http://localhost:3000/callback"

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.

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:

Terminal window
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:

Terminal window
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:

Terminal window
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_STATE

Copy the code:

Terminal window
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.

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:

Terminal window
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:

Terminal window
export ACCESS_TOKEN="ab12cd34..."
export REFRESH_TOKEN="ef34ab12..."
export ORG_ID="00000000-..."

Confirm the connection works, read the base currency, and check that you’re talking to the sandbox:

Terminal window
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
}
}

You need a contact ID and an income account ID to create an invoice. The demo seed has both — fetch them:

Terminal window
# A customer to bill
curl "https://app.accountfun.com/api/v1/contacts?type=customer&limit=5" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# An income account for the line item
curl 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.

Terminal window
export CONTACT_ID="00000000-..."
export ACCOUNT_ID="00000000-..."
Terminal window
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:

Terminal window
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").

Finish the loop by returning the sandbox to its fresh demo seed:

Terminal window
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.

Access tokens expire after 30 minutes. When yours does, use the refresh token to get a new pair:

Terminal window
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.

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.

  • 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.