# Build an app with FIZ

> Markdown edition of https://api.fiz.co/docs/apps. Português: https://api.fiz.co/docs/apps.md?lang=pt. Index: https://api.fiz.co/llms.txt

Connect your customers’ companies, create invoice drafts and add certified invoicing to your product. Start with a working example, then follow the same flow in your own server.

## 1. Run the example

The example is **your partner application**: an order-management screen with a customer, a service and an invoice action. FIZ appears as its invoicing provider. Set `APP_NAME` to your product’s name, matching the name registered in FIZ.

`localhost:3100` runs this partner app on your computer. Sign-in and registration happen at `app.fiz.co`; OAuth and REST requests go to `api.fiz.co`. The app connects directly to **production FIZ**.

You need **Node.js 22.17 or later**, Git and a [registered Development application](#2-register-your-application). There are no package dependencies and no API key.

```
git clone https://github.com/FIZ-co/partner-app-demo.git
cd partner-app-demo
cp .env.example .env
# Fill in APP_NAME and the three FIZ credentials, then:
npm start
# Open http://localhost:3100
```

Use the same app name as in FIZ and fill in your application ID, client ID and secret in the server’s `.env`:

```
APP_NAME="Your product name"
FIZ_APP_ID=YOUR_APPLICATION_ID
FIZ_CLIENT_ID=YOUR_CLIENT_ID
FIZ_CLIENT_SECRET=YOUR_CLIENT_SECRET
```

Open the order and choose **Connect FIZ** or **Create a FIZ account**. After consent, you return to the same order with your company connected. Choose its CAE and VAT category, then **Create invoice draft**. The order shows the saved FIZ draft ID.

> The example order is sample data; the FIZ connection is real. Creating its draft saves a real customer, service item and invoice draft in your chosen company. The example does not request `invoicing.issue` and has no issuance action.

Missing credentials show **Set up FIZ** with instructions and the correct callback URL for your port. They never start a fake FIZ connection. Keep the client secret on the server.

## 2. Register your application

Sign in to FIZ and open [Settings → Integrations → Developer apps](https://app.fiz.co/settings/integrations). If FIZ asks you to finish your account/company setup, complete it to open Settings. Creating an app does not require a paid plan. Choose **Development** for your first app.

| Setting | What to enter for the local example |
| --- | --- |
| Name / developer name | Your product and the person or company responsible for it. Both appear on consent. |
| Public website URL | `http://localhost:3100` — identifies your app on the consent screen. |
| Privacy policy URL | `http://localhost:3100/privacy` for the local example; publish your own policy for customers. |
| Logo URL (optional) | A publicly accessible image URL. FIZ stores the URL, not the image. Use HTTPS in production. |
| Connection start / resume URL (`installUrl`) | `http://localhost:3100/resume` — starts a fresh OAuth request after signup. |
| OAuth callback URLs (`redirectUris`) | `http://localhost:3100/callback` — receives the OAuth result. Matching is exact, including port, path and trailing slash. |
| Allowed scopes | `company.read`, `invoicing.read`, `invoicing.write` |
| Testers | Up to 20 email addresses. You can add an email before its FIZ account exists. The tester verifies it through normal email-code signup/sign-in; no extra tester confirmation is needed. |

### Three URLs with different jobs

For your hosted app, use addresses you control. These paths are examples, not FIZ endpoints:

| Field | Example | Purpose |
| --- | --- | --- |
| Public website URL | `https://your-app.example` | Your public product or marketing site, shown to the customer on consent. |
| Connection start / resume URL | `https://your-app.example/fiz/connect` | The page or handler that starts a fresh OAuth connection. FIZ returns here after profile setup and the AT step. |
| OAuth callback URLs | `https://your-app.example/fiz/callback` | Receives `code` or `error` after the customer allows or denies access. |

The connection start / resume URL is stored as `installUrl` (previously labelled “Installation URL”). It must actually resume the connection: a generic marketing page without this behavior leaves the customer stuck. The callback receives the result; it does not start the flow.

Copy the **application ID**, **client ID** (`fiz_app_…`) and **client secret**. The application ID identifies signup links; the client ID identifies OAuth requests. The secret is shown once. Store it on your server, never in browser JavaScript, mobile bundles or source control. FIZ stores only its hash. A lost secret requires explicit rotation.

### Development and Production are app types

**Development apps work on FIZ production**, for the creator and configured testers only. Each person still needs to own the company they connect. Development OAuth access does not require a paid API plan. It is real company data, not a fiscal sandbox: any authorized fiscal action remains real.

**Production apps** can connect other customers after FIZ review. All URLs must use public HTTPS; localhost is not allowed. Create a separate Production app when ready: its client ID, secret and customer grants are separate. An app’s environment cannot be changed.

You can keep up to 10 non-archived apps. Archive an unused app to free a slot; pausing does not free it. Register 1–10 callback URLs. Creation and review submission each have a shared limit of 10 attempts per account per clock hour.

## 3. Connect a company with OAuth

Use **Authorization Code + PKCE (S256)** with server-side client authentication. A FIZ API key belongs to one company; an app uses explicit OAuth consent for each customer connection. There is no app-only client-credentials flow.

1.  Your server creates state + PKCE
2.  Customer chooses a company and permissions in FIZ
3.  FIZ returns a code to your callback
4.  Your server exchanges it for tokens

|   |   |
| --- | --- |
| Issuer / REST resource | `https://api.fiz.co` |
| Discovery | [`/.well-known/oauth-authorization-server`](https://api.fiz.co/.well-known/oauth-authorization-server) |
| Authorize | `GET https://api.fiz.co/oauth/authorize` |
| Token | `POST https://api.fiz.co/oauth/token` |

### Start authorization

```
import { createHash, randomBytes } from 'node:crypto';

const state = randomBytes(32).toString('base64url');
const verifier = randomBytes(32).toString('base64url');
// Store state + verifier in the customer's server-side session with a short TTL.
const url = new URL('https://api.fiz.co/oauth/authorize');
url.search = new URLSearchParams({
  response_type: 'code',
  client_id: process.env.FIZ_CLIENT_ID,
  redirect_uri: 'http://localhost:3100/callback',
  state,
  code_challenge: createHash('sha256').update(verifier).digest('base64url'),
  code_challenge_method: 'S256',
  resource: 'https://api.fiz.co',
  scope: 'company.read invoicing.read invoicing.write',
}).toString();
// Redirect the customer's browser to url.href.
```

Keep the verifier and state in the initiating browser’s server-side session. They must be unpredictable, fresh for each attempt and never shared between users. The requested scopes must be allowed in your app settings.

### Validate the callback, then exchange the code

Require the original session, an exact matching `state`, the expected `iss=https://api.fiz.co`, and exactly one `code` or `error`. Reject duplicates and mixed success/error callbacks. A validated `error=access_denied` means the customer cancelled; show a retry option. Never exchange a code from an unverified callback.

```
curl --request POST https://api.fiz.co/oauth/token \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode "client_id=$FIZ_CLIENT_ID" \
  --data-urlencode "client_secret=$FIZ_CLIENT_SECRET" \
  --data-urlencode "code=$CODE" \
  --data-urlencode "code_verifier=$VERIFIER" \
  --data-urlencode 'redirect_uri=http://localhost:3100/callback' \
  --data-urlencode 'resource=https://api.fiz.co'
```

Send form-encoded parameters with `client_secret_post`, not JSON. Use the original verifier and identical redirect URI and resource. Authorization requests expire after 10 minutes; issued codes expire after 60 seconds and can be used once.

The response contains `access_token`, `refresh_token`, `expires_in`, `token_type` and granted `scope`. Store tokens on your server. Check the granted scopes rather than assuming every requested permission was accepted.

Every grant connects **one user, one company and one app**. Call `GET /company` and bind that company ID to your own customer before writing. A connection to another company requires a separate consent flow; no tenant header can switch a token’s company.

## 4. Bring a new customer to FIZ

### Who issues the invoice, and who pays FIZ?

**One app, a separate connection for each issuer.** Every merchant or courier issuing under their own NIF needs their own invoicing profile in FIZ and an authorized connection. In this journey they create or sign in to a FIZ account and set up their existing business or self-employed activity. “Create a company” means creating that profile in FIZ, not opening a new legal entity.

Your platform’s own company connection can issue only for that company. There is no per-invoice issuer-NIF parameter that makes it act for another merchant. A customer record and its NIF identify the *buyer*, not another issuer. Keep your application’s client ID and secret on your server, but store a separate token pair and `GET /company` binding for each connected issuer.

**Platform coverage removes the separate subscription requirement, not registration or consent.** When FIZ has enabled coverage for your Production app and the specific connection remains covered, the customer does not need a separate FIZ subscription for API access and fiscal issuance through your app. Coverage does not include manual paid FIZ features, declarations or API-key access. Ownership, granted scopes and AT readiness still apply. Discuss coverage and commercial terms with [FIZ support](mailto:support@fiz.co); app approval alone does not activate it.

### Start registration without sending the customer to plan selection

Use this partner registration link for new customers, not an ordinary signup link or a pending OAuth request. Use the application’s **registry ID**:

```
const signup = new URL('https://app.fiz.co/auth/signup');
signup.searchParams.set('path', '/auth/app-return?' +
  new URLSearchParams({ app: process.env.FIZ_APP_ID }));
// Link to signup.href. Use the application ID, not the fiz_app_ client ID.
```

The customer verifies their email, sets up their invoicing profile with their own NIF and reaches the AT connection step. They can connect AT or choose **Later**. The partner journey skips plan selection and the generic product-interest and discovery questions. Creating the profile does not subscribe the customer to a paid FIZ plan.

### Continue automatically after profile setup

FIZ then returns to your registered **connection start / resume URL** (`installUrl`). This return is not an OAuth callback: FIZ does not add an authorization code, access token, company ID, email or arbitrary return URL. In the demo, `/resume` handles this return and `/callback` receives the later OAuth result.

1.  FIZ: profile setup and AT step
2.  Your app: connection start / resume URL
3.  FIZ: company and permissions → Allow
4.  Your app: OAuth callback
5.  Your app: connection complete

1.  Before sending the customer to FIZ, preserve their identity and the intended connection in your own server-side session.
2.  Your registered start / resume handler restores that context. If the customer needs to sign in to your app again, continue from this handler after sign-in.
3.  Start a **fresh OAuth request with new state and PKCE**, as described in [Connect a company with OAuth](#3-connect-a-company-with-oauth). Reuse the logic behind your “Connect FIZ” button. The original request can expire during registration; do not reopen an old consent URL.
4.  FIZ shows the company and permissions. After Allow, validate the callback, exchange the code, bind the connection with `GET /company`, and show success in your app.

**The customer should not need to close the browser or press “Connect FIZ” again.** Test the entire journey with a new FIZ account, not only an existing account that already has a company.

If a signed-in customer starts OAuth before having any active company membership, consent offers **Set up my profile**. This enters the same partner journey and returns to your connection start / resume URL for fresh OAuth. Users who are members of a company but cannot authorize it still need its owner to connect. At consent, confirmed coverage is shown for the selected company; selecting another company or losing coverage changes that status.

AT credentials stay in FIZ. Your app uses `GET /company` to check readiness. Choosing Later allows a draft connection but does not bypass issuance requirements.

> Keep this journey in the same browser. The email-code message does not carry the partner return path. Completing registration from another device without the original link can lose the return and partner attribution; it does not assign a false partner. Restart from your partner link if needed.

## 5. Make your first API requests

### Check the connected company

```
curl https://api.fiz.co/company \
  --header "Authorization: Bearer $ACCESS_TOKEN"
```

The response contains `id`, `name`, `taxpayerNumber`, `cae` and `readiness`. It never exposes AT credentials, bank information or the owner’s personal details.

If `readiness.ready` is false, inspect `reasons[].code` and offer the returned `actionUrl` as a setup link: `AT_CREDENTIALS_REQUIRED`, `AT_SYNC_REQUIRED` or `SERIES_REQUIRED`. Refresh readiness after setup. The write endpoint still makes the final readiness check.

### Create this app’s customer and item

Send the following JSON to `POST /customers` and `POST /items`, with the bearer header, `Content-Type: application/json` and a distinct stable `Idempotency-Key` for each operation.

```
{
  "name": "Example customer",
  "country": "PT"
}
```

```
{
  "name": "Example service",
  "type": "SERVICE",
  "unitPrice": 10,
  "vatRate": "NORMAL"
}
```

Save both returned `id` values. Choose the VAT category and any exemption code applicable to the company; the example category is not a tax determination.

### Create a draft

Save this JSON as `draft.json`. Replace the sample IDs with the returned IDs and `cae` with a value from your company response. The item reference is `items[].id`, not `itemId`.

```
{
  "type": "INVOICE",
  "cae": "62010",
  "customerId": "aaaaaaaaaaaaaaaaaaaaaaaa",
  "items": [
    {
      "id": "bbbbbbbbbbbbbbbbbbbbbbbb",
      "quantity": 1
    }
  ]
}
```

```
curl --request POST https://api.fiz.co/invoices \
  --header "Authorization: Bearer $ACCESS_TOKEN" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: order-1042-draft' \
  --data @draft.json
```

Read it with `GET /invoices/{id}`. An `INVOICE` created through this endpoint is a draft. Issuance is a separate `POST /invoices/{id}/issue` action with `invoicing.issue`; provide the latest `expectedUpdatedAt` to avoid issuing a stale version. After issuance, `GET /invoices/{id}/pdf` retrieves the PDF.

> Issuing, settling or cancelling fiscal documents acts on the real company. Registered apps need verified AT credentials, enabled AT synchronization and valid series. FIZ does not show an extra confirmation screen for each partner API issuance. Your product must make the action clear to its user.

## 6. Understand permissions and isolation

| Scope | REST capability |
| --- | --- |
| `company.read` | Company identity and issuance readiness; list series. |
| `invoicing.read` | Read this app’s invoices, transport documents, customers and items; their PDFs. |
| `invoicing.write` | Create/update/remove this app’s drafts (invoices and transport documents), customers and items. |
| `invoicing.issue` | Issue, pay and cancel documents, transport documents included; create credit notes within this app’s permitted records. |
| `invoicing.read_all` | Read company-wide invoices, transport documents, customers and items. It does not permit editing another app’s or a manually created record. |

Choose only the permissions you need. Company metadata also accepts the existing invoicing read scope for compatibility; request `company.read` explicitly in new integrations. A scope for writing does not automatically grant reading.

By default, app A cannot read or modify app B’s records or manually created records. Create a separate customer and item record for your app even when the same buyer already exists in FIZ. The company owner still sees the records in FIZ.

The API reference includes API-key endpoints too. Registered-app OAuth currently supports **company, series, invoices, transport documents, customers and items**; scheduled invoices, templates, bank routes and pro formas are not opened by these REST scopes. Transport documents follow the invoice rules: an app sees and changes only the guias it created, sends loading and unloading addresses inline (the company’s saved addresses are not available to apps), and issuing or cancelling one needs `invoicing.issue`. A route without an OAuth permission explicitly assigned is denied.

The [MCP connector](https://api.fiz.co/docs/mcp.md) uses a different resource, `https://api.fiz.co/mcp`. REST tokens cannot call MCP and MCP tokens cannot call REST. Personal CIMD clients cannot obtain REST access. Registered apps cannot use the MCP pro forma tools.

## 7. Keep connections reliable

### Refresh tokens

Access tokens currently last one hour; use `expires_in` from the response. Refresh tokens last up to 90 days and rotate on use. Serialize refresh for each connection, including across your server replicas, and atomically save the new token pair.

```
curl --request POST https://api.fiz.co/oauth/token \
  --data-urlencode 'grant_type=refresh_token' \
  --data-urlencode "client_id=$FIZ_CLIENT_ID" \
  --data-urlencode "client_secret=$FIZ_CLIENT_SECRET" \
  --data-urlencode "refresh_token=$REFRESH_TOKEN" \
  --data-urlencode 'resource=https://api.fiz.co'
```

If a refresh response is lost, do not blindly reuse the old token: the server may have rotated it, and reuse can revoke the connection. Treat the outcome as uncertain and offer explicit reconnection. A confirmed temporary block keeps credentials for a later retry.

### Retry writes according to their recorded outcome

Send `Idempotency-Key` on every write for safe retries. Requests without it currently execute without replay protection; after a lost response, check the result before repeating an unkeyed request. Use a stable `Idempotency-Key` derived from your operation, such as `order-1042-draft`. Use different keys for customer creation, item creation and issuance. After a timeout, retry the *same* operation with the same key and identical body, path parameters and query to discover its recorded outcome. Do not treat every 409 as retryable.

Keys are 1–128 printable ASCII characters without spaces. Results are retained for 30 days and scoped to app + authorizing user + company + operation. Re-consent by the same user does not change that namespace; another user’s grant does. Keep your own durable order/document mapping. A replay includes `Idempotent-Replayed`.

**IN\_PROGRESS:** while the execution lease is active (currently 90 seconds), REST returns 409 with `Retry-After`. Wait and retry the same key. A completed operation can replay its saved result throughout the 30-day retention period, not just during the lease.

**ABANDONED / unknown outcome:** an expired lease without a reported result, or a failure after execution may have started, returns a different 409. Stop automatic retries. That key cannot start another execution; an explicitly abandoned record cannot replay a result either. Waiting does not make the key reusable. Check whether the original operation took effect before considering a new key.

For example, after `POST /invoices/{id}/issue`, read that same invoice and reconcile its status and number. If it was issued, save the result locally and do not issue again. Use a **new key only after confirming that no side effect occurred and the original operation is no longer running**. If that cannot be established, contact support instead of risking a duplicate. A validation rejection before execution leaves the original unchanged request retryable; a corrected payload is a new request and needs a new key.

### Pause, revoke and rotate

Pause, FIZ suspension or a Production app returning to review temporarily blocks use; credentials are retained. Archiving, disconnecting or removing a tester can revoke access. Narrowing allowed scopes invalidates grants with obsolete scopes; reconnect with the new permissions. Secret rotation immediately replaces the old secret; update your server configuration. It is not a substitute for revoking issued tokens.

Customers can disconnect in [Settings → Integrations](https://app.fiz.co/settings/integrations). Stop background work on a revoked connection and offer a deliberate reconnect action.

## 8. Diagnose common errors

| Result | What to do |
| --- | --- |
| Consent: app unavailable | Check enabled/archive/suspension state; Production approval; Development creator/tester identity; and requested scopes. A new tester can sign up with the listed email. |
| After signup: marketing page instead of consent | Check the app’s connection start / resume URL (`installUrl`). It must open your connection flow and start a new OAuth request, not just display your homepage. Register the correct handler and preserve the partner session through signup; keep the OAuth callback as the separate result endpoint. Test profile setup → AT → resume handler → consent → callback without manually restarting. |
| Consent: API plan required | A Production connection needs active platform coverage or a company plan with API access. Confirm coverage for this app and company with FIZ; do not ask a covered customer to buy another plan. Development testing is exempt from API-plan requirements, but not fiscal readiness or issuance rules. |
| `invalid_client` | Check client ID, current secret and form encoding. Do not trim or rewrite a secret silently. |
| `invalid_grant` | The code/token may be expired, consumed, revoked or no longer permitted. Reconnect explicitly; do not loop refresh. |
| `temporarily_unavailable` | Keep credentials, pause the job and retry later with backoff. Do not force every customer to reconnect. |
| REST 401 | Check expiry, revocation and exact audience. A suspended app also fails introspection. Distinguish a temporary block from a confirmed revoked grant. |
| REST 403 | Check granted scope, company membership/ownership, platform coverage or a Production plan, and whether the route supports OAuth. |
| REST 400 | Inspect the validation response and readiness. Check enum casing, CAE, customer/item IDs and required fields in the API reference. |
| REST 409 — IN\_PROGRESS | The response says the request is still being processed and includes `Retry-After`. Wait, then retry the **same key** and identical request. |
| REST 409 — ABANDONED / unknown outcome | The response says the earlier request may have taken effect; it has no retry timer. **Stop automatic retries: the key is spent for execution.** Check the existing document/records. Only use a new key once you confirm the first attempt had no effect and is no longer running; otherwise reconcile or contact support. These names describe internal outcomes; inspect the HTTP response message and `Retry-After`, not a nonexistent outcome field. |
| REST 422 | The same idempotency key was used for a different payload. Recover the original operation; do not keep changing keys to bypass the conflict. |
| 429 | Honor `Retry-After`, back off with jitter and stagger jobs. REST currently allows 300 requests/minute per grant/company; token requests have separate app/IP budgets. |

For support, provide application ID, environment, endpoint, UTC time, HTTP status and request/idempotency ID. Do not send client secrets, access/refresh tokens or AT credentials.

## 9. Prepare for customers

1.  Complete a Development connection as creator and as a listed tester, including a new-account signup that returns through your start / resume handler to consent and the callback without a second “Connect FIZ” click. Check cancellation, removed testers, company selection and retry behavior.
2.  Create a separate Production app with your real product name, public HTTPS URLs, privacy policy and only the required scopes.
3.  Submit it for FIZ review in Developer apps. Discuss questions through [FIZ support](mailto:support@fiz.co). Approval is required before customers can connect.
4.  Store secrets and tokens in server-side secret/encrypted storage. Replace the demo’s in-memory sessions with durable per-customer connections and a shared refresh lock.
5.  Verify company binding, idempotency, revocation and issuance readiness. Use your own UI for deliberate fiscal actions.
6.  Switch your server to the Production app’s credentials. Obtain fresh customer consent; Development grants are not migrated.

Editing approved app settings returns it to Draft and requires review again; pause/resume and secret rotation preserve review status. Arrange settings changes before inviting customers.

The example is a local learning server, not a deployable multi-customer service. Its tokens and request keys disappear on restart. A restart does not revoke FIZ grants. Do not rely on the example’s memory for production deduplication or recovery.

- [Open the example on GitHub ↗](https://github.com/FIZ-co/partner-app-demo)
- [Explore the API reference](https://api.fiz.co/)
