Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: BeeL. Public Customer Templates API
description: 'Public invoicing API for self-employed professionals in Spain with VeriFactu regulatory compliance. Authenticate using API Keys (`Authorization: Bearer `). Keys are scoped and environment-aware:
`beel_sk_live_*` targets production, `beel_sk_test_*` targets sandbox (no quota consumed, VeriFactu test mode). Every operation declares the scope it requires in its `x-required-scopes` extension, and the
reference at docs.beel.es shows it next to the operation''s title. Create the key with (at least)
those scopes; a key that is short of one is answered `403`. The full catalogue is in the
`ApiKeyAuth` security scheme below. OAuth2 is reserved for BeeL''s own official clients (the MCP bridge and the CLI) and is not part of
the public contract at present. There is no dynamic client registration and no way for a third
party to obtain a `client_id`, so the API key is the credential to integrate with. POST endpoints, and the PUT/PATCH/DELETE operations that list it, accept an `Idempotency-Key` header (any unique client-generated string) to retry a request safely without repeating it. A retry with the same key within 24 hours replays the first response when it was a success (2xx) or a server error (5xx), with the header `Idempotency-Replay: true`; a 4xx is not stored, so the corrected request can reuse the key. See the header parameter for the details. **This specification is served live by the API itself**, at
`https://app.beel.es/api/v1/openapi.yaml` (and `.json`) — the same document this reference is
built from, so you can point a generator or a viewer straight at it. **Response envelope.** Every JSON response is wrapped in `{success, data, meta}`. Bare
resources are not part of the contract. Read the payload through `data` and you never have to
branch on the shape of the body. - A **single resource** travels as an object in `data`: `data: {id, ...}`. - A **collection** travels under a named key inside `data`, alongside its `pagination`, also
inside `data`: `data: {invoices: [...], pagination: {...}}`. The key is named after the
resource, so a response never hides which collection it carries. - A collection is paginated unless the operation declares itself a **closed catalogue** — a
fixed, bounded list (tax types, customisation options) that has nothing to page through. Only
an explicit declaration in the operation makes a collection unpaginated; the absence of
`pagination` is never something to infer. - `GET /v1/accounts` paginates by **cursor** (`data: {accounts: [...], next_cursor}`) instead of
by page. That is a documented variant of pagination, not a different envelope: it is still a
named collection inside `data`. Which of the three a collection is, is declared and machine-readable, so no consumer has to
guess: the operation either carries `pagination` in its response schema, or the extension
`x-closed-catalogue: true`, or `x-pagination-style: cursor`. Those three cases are the whole
list, and a collection that declares none of them is a defect in this contract, not a fourth
variant to code around. A contract ratchet in CI enumerates exactly that. A `data` holding the array directly, with `pagination` as its sibling, is arguably the nicer
shape and is what a v2 of this contract would adopt. It is **not** what v1 does, and v1 will not
be flipped to it: live integrations read `data.` today, and no ergonomic gain justifies
breaking them without a version to move to. Operations on their way out are the one exception, and they are frozen rather than fixed. An
operation that is `deprecated` and announces a `Sunset` date keeps whatever shape it shipped
with — `GET /v1/products/search` is the last one whose `data` is a bare array — because
reshaping a route that is about to be removed only breaks the integrations still on it. Its
description says what the successor answers instead. Every operation without a retirement date
obeys the rule above. Query parameters are strict, across every endpoint.'
version: 1.9.0
contact:
name: BeeL. Technical Team
email: it@beel.es
url: https://beel.es
license:
name: Proprietary
url: https://beel.es/license
x-agent-resources:
openapiJson: https://beel.es/openapi.json
openapiYaml: https://beel.es/openapi.yaml
source: https://docs.beel.es/api/openapi
authorizationServerMetadata: https://app.beel.es/api/.well-known/oauth-authorization-server
llmsFull: https://docs.beel.es/llms-full.txt
mcpServer: https://mcp.beel.es/mcp
x-onboarding:
signupUrl: https://app.beel.es/signup
selfServeApiKeys: true
freeTier: sandbox ilimitado, sin tarjeta
sandbox:
keyPrefix: beel_sk_test_
sameBaseUrl: true
aeatTestMode: true
quotaConsumed: false
liveKeyPrefix: beel_sk_live_
salesContactRequired: false
zeroAuthEndpoints:
- https://beel.es/openapi.json
- https://beel.es/api/health
- https://beel.es/agents.md
x-cli:
package: '@beel_es/cli'
npm: https://www.npmjs.com/package/@beel_es/cli
quickstart: npx @beel_es/cli --help
x-agent-instructions: https://beel.es/agents.md
servers:
- url: https://app.beel.es/api
description: Production server
security:
- ApiKeyAuth: []
- OAuth2: []
tags:
- name: Customer Templates
description: Templates and sample files for customer imports
paths:
/v1/customers/templates/csv:
post:
tags:
- Customer Templates
summary: Download the customer import template
deprecated: true
x-deprecation: nif-migration
description: 'Downloads a sample CSV for customer import: the required headers plus example rows, written
with a UTF-8 byte order mark so that Excel opens it with the accents intact.
- **Deprecated:** use `GET /v1/templates/customer-import`, which returns the same file.
Reading a fixed template is not a `POST`, and the template belongs to no customer.
- **Retirement:** the response announces the retirement date in its `Sunset` header.
**Retires on 9 December 2026.** See the migration guide for what moved where and what changes when you switch.'
operationId: downloadCustomerTemplateCsv
x-successor:
method: GET
path: /v1/templates/customer-import
x-required-scopes:
- customers:read
security:
- ApiKeyAuth: []
- OAuth2:
- customers:read
responses:
'200':
description: CSV template downloaded successfully
content:
text/csv:
schema:
type: string
format: binary
example: 'nombre_fiscal;nif;email;telefono;direccion_calle;direccion_numero;direccion_codigo_postal;direccion_poblacion;direccion_provincia
"Construcciones García y Asociados SL";B12345674;contacto@construccionesgarcia.es;915551234;"Calle Gran Vía";45;28013;Madrid;Madrid
'
headers:
Content-Disposition:
description: File name for download
schema:
type: string
example: attachment; filename="template_clientes_beel.csv"
Content-Type:
description: File MIME type
schema:
type: string
example: text/csv; charset=utf-8
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/TooManyRequests'
'500':
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/UnexpectedError'
components:
schemas:
ResponseMeta:
type: object
properties:
timestamp:
type: string
format: date-time
example: '2025-01-15T10:30:00Z'
request_id:
type: string
example: 4bf92f3577b34da6a3ce929d0e0e4736
ErrorResponse:
type: object
description: "Error response shared by all BeeL. APIs.\n\nThe payload carries **two contracts at once** (additive, non-breaking):\n\n- **Legacy** (`success`, `error.{code,message,details}`, `meta`) — kept\n intact for existing consumers.\n- **RFC 9457** (`type`, `title`, `detail`, `instance`) — new fields\n for integrators following Problem Details for HTTP APIs. The\n `type` URI is the stable, shareable link to the error's\n documentation page (e.g. `https://docs.beel.es/errors/{code}`).\n\nFuture migration: the legacy fields will be deprecated via\n`Deprecation`/`Sunset` headers after a sufficient adoption window,\nand the response Content-Type will move to\n`application/problem+json`.\n"
required:
- success
- error
properties:
success:
type: boolean
example: false
error:
$ref: '#/components/schemas/ErrorDetail'
meta:
$ref: '#/components/schemas/ResponseMeta'
type:
type: string
format: uri
description: 'Stable URI that identifies the problem type and where the
integrator will find its documentation.
'
example: https://docs.beel.es/errors/INVOICE_NO_LINES
title:
type: string
description: 'Short summary of the problem type. Stable between occurrences
of the same `type`.
'
example: INVOICE_NO_LINES
detail:
type: string
description: 'Specific message for this occurrence, localized according to
`Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy
`error.message`.
'
example: The invoice must have at least one line
instance:
type: string
format: uri-reference
description: 'URI that identifies the specific occurrence of the problem —
typically the path of the affected resource.
'
example: /v1/invoices/abc-123
ErrorDetail:
type: object
required:
- code
- message
properties:
code:
type: string
example: VALIDATION_ERROR
message:
type: string
example: The provided data is not valid
details:
type: object
additionalProperties: true
example:
field: specific error message
responses:
TooManyRequests:
description: 'Rate limit exceeded. `RATE_LIMIT_EXCEEDED` when the request rate of your credential is
over its limit; `TOO_MANY_REQUESTS` when an email the operation sends is held back because
too many went out to that recipient in a short time. Either way, retry after the
`Retry-After` seconds.
`QUOTA_EXCEEDED` is the other `429`: the operation would go over a quota of your plan
for the current period (issued invoices, for instance). Retrying does not help until the
quota resets or the plan changes. `error.details` carries `feature_code`,
`current_usage`, `quota_limit` and `reset_date`, and `RateLimit-Reset` the seconds until
the reset when it is known.
'
headers:
Retry-After:
description: Seconds until the rate limit resets
schema:
type: integer
RateLimit-Limit:
description: Maximum requests allowed in the window
schema:
type: integer
RateLimit-Remaining:
description: Remaining requests in the current window
schema:
type: integer
RateLimit-Reset:
description: Seconds until the current window resets
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
success: false
error:
code: RATE_LIMIT_EXCEEDED
message: Too many requests. Please try again in 60 seconds.
meta:
timestamp: '2025-01-15T10:30:00Z'
request_id: 4bf92f3577b34da6a3ce929d0e0e4736
UnexpectedError:
description: 'Any status code the operation does not list above. Every operation declares it, so a
generated client always has a branch to fall into and never loses the cause of a failure
it did not anticipate.
This is where the transport-level answers land — `405`, `406`, `415` and `429` — together
with any status a future version of the API starts returning. All of them carry the same
error envelope as the codes listed explicitly, so `error.code` is what tells them apart:
switching on the status code alone is not enough. See «Transport-level errors» in the
API description for when each one is produced.
A `402` carrying `SUBSCRIPTION_REQUIRED` lands here when the account has no active
subscription. `error.details` says why and what to do: `current_status` (absent if it
never had one), `required_permission` and `action`.
A `503` carrying `SERVICE_UNAVAILABLE` lands here too, with a `Retry-After` header: a
dependency of BeeL itself (the database, the email provider) was momentarily
unavailable and nothing was done. Retry after those seconds.
A `502` carrying `EXTERNAL_SERVICE_ERROR` also lands here: an outbound integration the
operation depends on failed or did not answer in time. It is a transient condition — retry
with the same `Idempotency-Key` where the operation accepts one.
One exception to the envelope: a failure of the network edge that never reaches the
application (`502`, `503`, `504`, `524`) is generated by Cloudflare and its body is not
BeeL''s — it may not even be JSON. Treat those as "no answer", and retry.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
success: false
error:
code: UNSUPPORTED_MEDIA_TYPE
message: 'Unsupported media type: text/plain. Supported: application/json'
meta:
timestamp: '2025-01-15T10:30:00Z'
request_id: 4bf92f3577b34da6a3ce929d0e0e4736
InternalServerError:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
success: false
error:
code: INTERNAL_ERROR
message: Internal server error
meta:
timestamp: '2025-01-15T10:30:00Z'
request_id: 4bf92f3577b34da6a3ce929d0e0e4736
Forbidden:
description: "Authenticated but not allowed. Fourteen causes shared by every operation, told apart by\n`error.code`; an operation that adds a cause of its own names it in its `403`. You can\nbranch on them exhaustively.\n\n- `INSUFFICIENT_SCOPE` — the credential lacks a scope the operation requires;\n `error.details.missing_scopes` names them as a single comma-separated string (for\n example `\"invoices:write,customers:read\"`), not as an array; `required_scopes` has the\n same shape and lists every scope the operation needs. Retrying will not help: mint a key\n that holds them.\n- `COMPANY_READ_ONLY` — the scope is there, but your access level over that NIF only\n lets you read it.\n- `ACCOUNT_MANAGEMENT_FORBIDDEN` — the scope is there, but your role over the account,\n or the management relationship you hold over it, does not cover this operation.\n- `ACCOUNT_NOT_ACCESSIBLE` — the account is not yours to reach, which is also the answer\n when it does not exist, so existence is never disclosed.\n- `ACTIVE_COMPANY_NOT_ACCESSIBLE` — the same, for a NIF: the company in the path, or the\n one named by `BeeL-Active-Company`, is not one this credential may reach — and again,\n this is also the answer when it does not exist.\n- `COMPANY_ACCESS_REVOKED` — your access to the NIF was withdrawn while the request was\n already in flight, so the write was rejected and nothing was recorded. Retrying will\n not help until the access is granted again.\n- `LIVE_CREDENTIAL_REQUIRED` — the operation changes the real account and the call came\n from a test API key (`beel_sk_test_…`). Use your live key or the dashboard.\n- `FEATURE_NOT_AVAILABLE` — your subscription does not include the entitlement the\n operation needs; `error.details.feature_code` names it. This gate runs **before** the\n scope gate, so for such an operation you never see `INSUFFICIENT_SCOPE` first.\n- `NO_ACTIVE_ACCOUNT` — the credential does not resolve to an account, so no scope can be\n evaluated against one. Fail-closed, not a permission that can be granted to you.\n- `OPERATION_REQUIRES_SESSION` — the operation is available only from the web session; no\n API key and no OAuth2 token can perform it, whatever scopes it holds.\n- `OPERATION_REQUIRES_API_KEY` — the reverse: the operation needs a scope that only an API\n key can hold (such as `accounts:write`), so a dashboard session never satisfies it.\n- `API_KEY_LIVE_NOT_AVAILABLE` — the call came with a live credential (a `beel_sk_live_…`\n API key, or an OAuth2 token issued without the `sandbox` scope) and the account has no\n Live mode yet. It is never degraded to Test: use test credentials, or activate Live\n first.\n- `ACCOUNT_REQUIRES_FOCUS_SWITCH` — dashboard session only: you are a member of the account\n in the path, but it is not the account in focus. Switch focus to it and retry.\n- `REQUEST_BLOCKED` — the edge security filter stopped the request before the API saw its\n body, usually because a text field carries something that looks like markup or a\n script (HTML tags, `<script>`). Review the body and retry.\n"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
success: false
error:
code: FORBIDDEN
message: You do not have permission to access this resource
meta:
timestamp: '2025-01-15T10:30:00Z'
request_id: 4bf92f3577b34da6a3ce929d0e0e4736
Unauthorized:
description: 'Missing or invalid authentication. Like every other error, `message`/`detail` is
localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English when the
header is missing or asks for none of those.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
success: false
error:
code: UNAUTHORIZED
message: Authentication is required to access this resource
meta:
timestamp: '2025-01-15T10:30:00Z'
request_id: 4bf92f3577b34da6a3ce929d0e0e4736
securitySchemes:
ApiKeyAuth:
type: http
scheme: bearer
bearerFormat: beel_sk_*
description: 'Keys are prefixed `beel_sk_`, and each one carries the scopes it was created with: a key
short of the scope an operation needs is answered `403`. The scope an operation requires
is shown next to its title, and the full catalogue lives in the Scopes reference.
Keys are created from the BeeL dashboard. They are secret credentials: do not share them
or commit them to source control.
'
SessionCookie:
type: apiKey
in: cookie
name: BEEL_SESSION
description: 'Session Cookie authentication (for browser-based access).
**Note:** This method is primarily used by the web application.
For programmatic access, use API Keys instead.
'
OAuth2:
type: oauth2
description: 'Access tokens issued by the BeeL. authorization server (issuer `https://app.beel.es/api`, metadata at https://app.beel.es/api/.well-known/oauth-authorization-server) are accepted exactly like an API key: `Authorization: Bearer <access_token>`.
Authorization code with PKCE (S256) and refresh tokens. Request only the scopes the operation declares — a token short of one is answered `403 INSUFFICIENT_SCOPE` with `error.details.missing_scopes`.
Scope catalogue and semantics: https://docs.beel.es/auth/scopes.'
flows:
authorizationCode:
authorizationUrl: https://app.beel.es/api/oauth2/authorize
tokenUrl: https://app.beel.es/api/oauth2/token
refreshUrl: https://app.beel.es/api/oauth2/token
scopes:
accounts:read: Read provisioned (managed) accounts
accounts:write: Create, update and delete provisioned (managed) accounts
companies:list: List companies (NIFs) and their stats
companies:read: Read companies (NIFs)
companies:write: Create, update and delete companies (NIFs)
configuration:read: Read account and fiscal configuration
configuration:write: Create, update and delete account and fiscal configuration
custom-fields:read: Read custom-fields
custom-fields:write: Create, update and delete custom-fields
customers:read: Read customers
customers:write: Create, update and delete customers
email-domains:read: Read email-domains
email-domains:write: Create, update and delete email-domains
emails:read: Read email deliveries
invoices:read: Read invoices
invoices:write: Create, update and delete invoices
logs:read: Read API request logs
members:read: Read members and invitations
members:write: Create, update and delete members and invitations
nif:validate: Validate Spanish tax IDs (NIF/CIF) against the AEAT census
payment-connections:read: Read payment connections (Stripe)
payment-connections:write: Create, update and delete payment connections (Stripe)
products:read: Read products
products:write: Create, update and delete products
sandbox: Operate in the sandbox environment (test keys, AEAT test mode, no quota consumed)
series:read: Read invoice numbering series
series:write: Create, update and delete invoice numbering series
webhooks:read: Read webhook subscriptions
webhooks:write: Create, update and delete webhook subscriptions
x-scope-catalog:
- name: invoices:read
grants: Read invoices
- name: invoices:write
grants: Create, update and delete invoices
- name: customers:read
grants: Read customers
- name: customers:write
grants: Create, update and delete customers
- name: products:read
grants: Read products
- name: products:write
grants: Create, update and delete products
- name: custom-fields:read
grants: Read invoice custom fields and their options
- name: custom-fields:write
grants: Create, update and archive invoice custom fields and their options
- name: members:read
grants: Read the account's members
- name: members:write
grants: Change the role of and remove the account's members
- name: accounts:read
grants: Read the accounts you manage and their provisioning usage (privileged)
- name: accounts:write
grants: Provision accounts on behalf of their holders (privileged)
- name: account:admin
grants: Manage the account's own credentials and billing (privileged)
- name: webhooks:read
grants: Read webhook subscriptions and their deliveries
- name: webhooks:write
grants: Create, update and delete webhook subscriptions, rotate secrets and retry deliveries
- name: configuration:read
grants: Read configuration (VeriFactu, taxes, preferences)
- name: configuration:write
grants: Update configuration (VeriFactu, taxes, preferences)
- name: series:read
grants: Read invoice series
- name: series:write
grants: Create, update and delete invoice series
- name: nif:validate
grants: Validate NIFs against AEAT
- name: companies:read
grants: Read companies
- name: companies:write
grants: Manage companies
- name: companies:list
grants: List the account's companies (NIFs)
- name: payment-connections:read
grants: Read the payment connections of a managed NIF
- name: payment-connections:write
grants: Connect and disconnect payment providers for a managed NIF
- name: emails:read
grants: Read the email delivery history
- name: email-domains:read
grants: Read the account's email sending domains and the DNS records to publish for each
- name: email-domains:write
grants: Register, verify, remove and choose the default of the account's email sending domains; it changes the address your invoices are sent from
- name: logs:read
grants: 'Read the API request logs. Its reach is the PERSON, not the key: a credential with this scope reads the requests — bodies and headers included — made with ANY of your API keys in that environment, including your privileged ones. It never crosses to another user or another account.'
x-tagGroups:
- name: Invoices
x-icon: FileText
tags:
- CompanyInvoices
- CompanyInvoiceLifecycle
- CompanyInvoiceDelivery
- Invoices
- InvoiceLifecycle
- InvoiceDelivery
- name: Proforma
x-icon: FileClock
tags:
- CompanyProforma
- Proforma
- name: Customers
x-icon: Users
tags:
- CompanyCustomers
- CompanyCustomerImports
- Templates
- Customers
- Customer Import
- Customer Templates
- name: Products
x-icon: Package
tags:
- CompanyProducts
- Products
- name: Custom Fields
x-icon: Tags
tags:
- CompanyCustomFields
- name: Members
x-icon: Users
tags:
- Members
- name: Invitations
x-icon: Mail
tags:
- Invitations
- name: Accounts
x-icon: Building2
tags:
- Accounts
- AccountImports
- Provisioning
- name: Invoice Series
x-icon: List
tags:
- CompanySeries
- InvoiceSeries
- name: Tax Configuration
x-icon: Calculator
tags:
- CompanyTaxConfiguration
- TaxTypes
- ConfigurationTax
- name: VeriFactu
x-icon: ShieldCheck
tags:
- CompanyVeriFactuConfiguration
- ConfigurationVeriFactu
- name: Preferences
x-icon: Settings
tags:
- ConfigurationPreferences
- name: Invoice Customization
x-icon: Palette
tags:
- InvoiceCustomization
- name: NIF Validation
x-icon: Search
tags:
- NIF
- name: Companies
x-icon: Building2
tags:
- CompanyRepresentation
- AccountCompanies
- Company
- CompanyActivations
- PublicCompanyRepresentations
- CompanyBranding
- name: Recurring Invoices
x-icon: Repeat
tags:
- CompanyRecurringInvoices
- RecurringInvoices
- name: Payment Connections
x-icon: CreditCard
tags:
- CompanyPaymentConnections
- name: Payment Events
x-icon: Receipt
tags:
- CompanyPaymentEvents
- name: Webhooks
x-icon: Webhook
tags:
- AccountWebhooks
- Webhooks
- name: Request Logs
x-icon: ScrollText
tags:
- AccountRequestLogs
- RequestLogs
- name: Emails
x-icon: Mail
tags:
- Emails
- AccountEmails
- AccountEmailDomains
- CompanyEmailConfiguration
- EmailTemplateVariables
- name: Identity
x-icon: Fingerprint
tags:
- Me
- Identity
- name: Fiscal Summary
x-icon: Calculator
tags:
- CompanyFiscalSummary