# Sample app

> A runnable OAuth 2.0 integration in Node.js or Python — connect, refresh, and create a draft invoice end-to-end.

Source: https://www.accountfun.com/docs/api/sample-app/

The [API quickstart](https://www.accountfun.com/docs/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:

[Download Node.js + Express sample](https://www.accountfun.com/docs/samples/node-oauth-demo.zip)Single server.js file, Express + express-session, no external HTTP client (uses the built-in fetch).

[Download Python + Flask sample](https://www.accountfun.com/docs/samples/python-oauth-demo.zip)Single app.py file, Flask + requests, server-side token store.

## What the sample does

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

## Prerequisites

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](https://www.accountfun.com/docs/api/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.

## Run it

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

**Node.js**

Terminal

```bash
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`).

**Python**

Terminal

```bash
cd python-oauth-demo
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env   # then add your CLIENT_ID / CLIENT_SECRET
python app.py          # → http://localhost:5000
```

Requires Python 3.10 or newer.

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.

## How it works

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.

### 1. Send the user to authorize

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

**Node.js**

```js
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}`);
```

**Python**

```python
verifier, challenge = make_pkce()
state = secrets.token_urlsafe(16)
session["oauth_verifier"] = verifier
session["oauth_state"] = state


params = {
    "response_type": "code",
    "client_id": CLIENT_ID,
    "redirect_uri": REDIRECT_URI,
    "scope": SCOPES,
    "state": state,
    "code_challenge": challenge,
    "code_challenge_method": "S256",
}
url = requests.Request(
    "GET", f"{ACCOUNTFUN_API_URL}/api/v1/oauth/authorize", params=params
).prepare().url
return redirect(url)
```

### 2. Exchange the code for tokens

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

**Node.js**

```js
// 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);
```

**Python**

```python
# Never exchange a code without confirming state came back unchanged.
if state != session.pop("oauth_state", None):
    abort(400)


tok = post_token({
    "grant_type": "authorization_code",
    "code": code,
    "redirect_uri": REDIRECT_URI,
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET,
    "code_verifier": session.pop("oauth_verifier", None),
})
_save_tokens(tok)
```

### 3. Keep the connection alive

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.

**Node.js**

```js
// 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
```

**Python**

```python
# One in-flight refresh per session — a second concurrent rotation would
# invalidate the entire token family.
tok = post_token({
    "grant_type": "refresh_token",
    "refresh_token": TOKEN_STORE[sid]["refresh_token"],
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET,
})
_save_tokens(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.

### 4. Create a draft invoice

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.

**Node.js**

```js
// 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
```

**Python**

```python
# Date input values are already calendar dates in "YYYY-MM-DD" form.
r = api_request("POST", "/api/v1/invoices", json_body={
    "contactId": contact_id,
    "issueDate": issue_date,
    "dueDate": due_date,
    "lines": [
        {"description": description, "quantity": "1",
         "unitAmount": "50.00", "accountId": account_id},
    ],
})
invoice = r.json()["invoice"]  # 201 Created
```

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

## Production notes

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

## Where to go next

[Authentication](https://www.accountfun.com/docs/api/authentication/)The complete OAuth 2.0 reference — PKCE, refresh rotation, revocation, and the role ceiling.

[Invoices](https://www.accountfun.com/docs/api/invoices/)Full endpoint reference for listing, creating, and fetching invoices.

[Errors & rate limits](https://www.accountfun.com/docs/api/errors-and-limits/)Status codes, error shapes, and rate-limiting behavior for resilient error handling.
