FIZ / developers

Integration guide

Build an app with FIZ

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

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

SettingWhat to enter for the local example
Name / developer nameYour product and the person or company responsible for it. Both appear on consent.
Website URLhttp://localhost:3100
Privacy policy URLhttp://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.
Installation URLhttp://localhost:3100/resume — starts a fresh OAuth request after signup.
Redirect / callback URLhttp://localhost:3100/callback — receives the OAuth result. Matching is exact, including port, path and trailing slash.
Allowed scopescompany.read, invoicing.read, invoicing.write
TestersUp 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.

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 resourcehttps://api.fiz.co
Discovery/.well-known/oauth-authorization-server
AuthorizeGET https://api.fiz.co/oauth/authorize
TokenPOST 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

Use the application’s registry ID to start partner registration:

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, creates a company and reaches the AT connection step. They can connect AT or choose Later. The partner journey skips the generic product-interest and discovery questions.

FIZ then returns to your registered installation URL. It does not add an access token, company ID, email or arbitrary return URL. Your installation handler starts a fresh OAuth authorization with new state and PKCE. In the example this is /resume; /callback is a different endpoint.

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.

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.

6. Understand permissions and isolation

ScopeREST capability
company.readCompany identity and issuance readiness; list series.
invoicing.readRead this app’s invoices, customers and items; invoice PDF.
invoicing.writeCreate/update/remove this app’s drafts, customers and items.
invoicing.issueIssue, pay and cancel documents; create credit notes within this app’s permitted records.
invoicing.read_allRead company-wide invoices, 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, customers and items; transport documents, scheduled invoices, templates, bank routes and pro formas are not opened by these REST scopes. A route without an OAuth permission explicitly assigned is denied.

The MCP connector 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

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. Stop background work on a revoked connection and offer a deliberate reconnect action.

8. Diagnose common errors

ResultWhat to do
Consent: app unavailableCheck enabled/archive/suspension state; Production approval; Development creator/tester identity; and requested scopes. A new tester can sign up with the listed email.
Consent: API plan requiredA Production connection needs a company plan with API access. Development testing is exempt; it does not change the company’s subscription.
invalid_clientCheck client ID, current secret and form encoding. Do not trim or rewrite a secret silently.
invalid_grantThe code/token may be expired, consumed, revoked or no longer permitted. Reconnect explicitly; do not loop refresh.
temporarily_unavailableKeep credentials, pause the job and retry later with backoff. Do not force every customer to reconnect.
REST 401Check expiry, revocation and exact audience. A suspended app also fails introspection. Distinguish a temporary block from a confirmed revoked grant.
REST 403Check granted scope, company membership/ownership, Production plan and whether the route supports OAuth.
REST 400Inspect the validation response and readiness. Check enum casing, CAE, customer/item IDs and required fields in the API reference.
REST 409 — IN_PROGRESSThe 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 outcomeThe 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 422The same idempotency key was used for a different payload. Recover the original operation; do not keep changing keys to bypass the conflict.
429Honor 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. 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. 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.