Skip to content

Authentication and scopes ​

The public API (/api/v1/*) accepts API keys and OAuth2 tokens in the Authorization: Bearer header. Use an API key for your own server integration; use OAuth2 when another app needs a user to grant access to their company. The BFF (/app/*) uses the browser's HTTPS cookie session; OAuth2 tokens cannot authenticate to it.

API key ​

The public API uses a tenant API key, sent as a Bearer token:

Authorization: Bearer bk_live_xxxxxxxxxxxxxxxx
  • bk_live_… → production tenant.
  • bk_test_… → test tenant, submitted to the AEAT test services. See the test environment.

A missing or invalid key returns 401; a key without the required scope returns 403.

OAuth2 for third-party apps ​

Third-party apps can also connect through authorization code with PKCE S256. A Bilteo administrator registers the app in the admin with its name, exact redirect URIs, and maximum allowed scopes. The client_id is public; there is no client secret. The approving user needs api_keys.admin on the selected tenant.

Generate a random 43–128-character code_verifier, its unpadded base64url(SHA256(code_verifier)) challenge, and a random state of at least 16 characters. Redirect the browser to the application page https://<app-host>/oauth/authorize with response_type=code, client_id, redirect_uri, space-separated scope, state, code_challenge, and code_challenge_method=S256. After consent, verify the returned state and exchange the code once within five minutes. Bilteo lets the user choose the tenant; you may provide tenant_id if known:

If the user declines, Bilteo redirects to redirect_uri with error=access_denied and the original state.

Example authorization URL (URL-encode each query parameter; encode spaces in scope as %20):

text
https://<app-host>/oauth/authorize?response_type=code&client_id=<uuid>&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=invoices%3Aread%20partners%3Aread&state=<random-value>&code_challenge=<base64url-challenge>&code_challenge_method=S256

Exchange the returned code:

bash
curl -X POST https://api.test.bilteo.com/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'client_id=<uuid>' \
  --data-urlencode 'code=<returned-code>' \
  --data-urlencode 'redirect_uri=https://app.example.com/callback' \
  --data-urlencode 'code_verifier=<verifier-original>'

The response contains access_token, token_type (Bearer), expires_in (900 seconds), refresh_token, and scope.

Send Authorization: Bearer bo_at_... to /api/v1/*. Access tokens last 15 minutes. Refresh with POST /oauth/token and form fields grant_type=refresh_token, client_id, and refresh_token=bo_rt_.... Store the new refresh token after each use; replay of an old one revokes the whole connection. Refresh tokens expire after 30 days. To disconnect, send client_id and token (access or refresh) as a form to POST /oauth/revoke. Server metadata is at /.well-known/oauth-authorization-server.

bash
curl -X POST https://api.test.bilteo.com/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=refresh_token' \
  --data-urlencode 'client_id=<uuid>' \
  --data-urlencode 'refresh_token=bo_rt_...'

curl -X POST https://api.test.bilteo.com/oauth/revoke \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'client_id=<uuid>' \
  --data-urlencode 'token=bo_rt_...'

OAuth2 and API keys use exactly the same scopes and API endpoints. An OAuth token is bound to the tenant selected at consent: do not use X-Tenant to switch it. Missing or expired credentials return 401; missing scopes return 403. See authentication errors.

Credential identity ​

GET /api/v1/me tells you which company the credential is bound to. It works with API keys and OAuth tokens and requires no scope.

json
{
  "tenant": {
    "id": "…",
    "name": "Example Salon SL",
    "issuer_name": "Example Salon SL",
    "issuer_id_num": "B12345678",
    "livemode": true
  },
  "plan_code": "pro",
  "auth": { "type": "oauth", "scopes": ["invoices:read", "invoices:write"] }
}

livemode is false for the test tenant. With OAuth, call it right after getting the token. The user picks the company in Bilteo and the token response does not include it.

Scopes ​

Each API key or OAuth connection has a set of scopes that limits what it can do. Grant only the ones it needs. Scopes follow resource:action; read-only catalogues only expose :read.

ScopeAllows
invoices:readRead invoices and their lines
invoices:writeCreate and edit drafts and lines, and create corrective invoices
invoices:issueIssue invoices: number them and submit them to the AEAT
invoices:cancelCancel issued invoices
invoices:replace_simplifiedCreate or issue F3 invoices that replace F2 invoices and manage replace relations
invoices:fixCorrect the Veri*Factu record of an issued invoice: reopens it (needs_fix) if never accepted, or queues the correction directly if it was; used with invoices:issue
verifactu:readRead Veri*Factu records and Verifactu Direct API external invoices
verifactu:writeRegister, cancel, and correct external invoices through Verifactu Direct API
partners:read / partners:writeRead or manage customers
products:read / products:writeRead or manage products
series:read / series:writeRead or manage numbering series
economic_activities:read / economic_activities:writeRead or manage economic activities
taxes:readRead the tax catalogue
fiscal_positions:readRead the fiscal-position catalogue
payment_terms:readRead the payment terms (read only)
usage:readRead the effective plan, limits, and current usage
webhooks:readList and inspect webhook endpoint settings (without the secret)
webhooks:writeCreate, update, and delete endpoints; rotate their secret

Routes are the source of truth

Every operation in the API reference declares its required scope. If this table and the reference ever differ, the generated API reference takes precedence.

Correcting a rejected record also requires issuance

POST /invoices/{id}/actions/request-fix requires both invoices:fix and invoices:issue: reopening only makes sense if the key can submit the corrected record. See Correcting rejected records.

Bilteo · Documentation