NewStripe: a buyer with a NIF always gets a standard invoice, and the simplified threshold is deprecated
BeeL
Get startedMulti-NIFVeriFactuRulesStripeAPI referenceChangelog
Customers

Create a customer for a company

Scopecustomers:write

Creates a new customer under this company.

  • Idempotency-Key: it identifies the same operation on the deprecated flat route, so a retry that switches route replays instead of creating twice.

POST
/v1/companies/{company_id}/customers
AuthorizationBearer <token>

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.

In: header

Path Parameters

company_idstring

Unique identifier (UUID) of the company the operation acts on — its identifier, not its NIF. It is the only source of context: the account that owns it is derived from it, and the BeeL-Active-Company header plays no part. A company you do not reach answers 403, and so does a company that does not exist, so the existence of a company in another account is never disclosed.

Formatuuid

Header Parameters

Idempotency-Key?string

Idempotency key to prevent duplicates in sensitive operations.

  • Any unique client-generated string (e.g. an order id). A UUID also works but is not required
  • Allowed characters: letters, digits, _ and - (max 255 chars)
  • Retrying with the same key replays the first response when it was a success (2xx) or a server error (5xx): same status and body, plus the header Idempotency-Replay: true. After a 5xx, check whether the operation took effect before retrying with a new key
  • A 4xx is not stored: the key is released, so the corrected request can reuse it
  • Stored responses expire 24 hours after processing

The key is scoped per user and environment, and bound to the request body, so retrying after a network timeout replays the stored response instead of repeating the operation.

StatusCodeWhen
400INVALID_IDEMPOTENCY_KEYThe key breaks the format rules above.
409IDEMPOTENCY_KEY_PROCESSINGThe first request is still in flight. Wait for the Retry-After seconds (2) and retry with the same key.
409IDEMPOTENCY_KEY_MISMATCHThe key was already used with a different body. Use a new key.
Match^[a-zA-Z0-9_-]+$
Lengthlength <= 255
legal_namestring

Customer legal name (required).

When you also send a Spanish nif, how it is used depends on the customer: for an individual the AEAT census matches NIF and name together, so a name it does not recognise makes the customer invalid; for a company the name is not verified and only the CIF decides.

Match^\S.*$
Length1 <= length <= 120
trade_name?string|null

Customer trade name. Optional, but not empty by default: leave it out on creation and it is filled with legal_name, which is what then shows as the recipient's trade name on the invoice PDF. Send it explicitly if the two differ.

The default applies on creation only. A PUT replaces the customer whole, so omitting trade_name there clears it instead of refilling it from legal_name.

Lengthlength <= 120
nif?string
alternative_id?|null

Alternative identifier for customers without Spanish Tax ID.

VeriFactu rules (checked when you send it, 422 on violation)

  • country_code is required, except for PASSPORT (03) and NOT_REGISTERED (07), the two types AEAT accepts with ES: omitted, ES applies. Missing for any other type, the identifier is rejected with ALTERNATIVE_ID_COUNTRY_REQUIRED, and error.details names the field where it was sent: alternative_id.country_code on a customer, recipient.alternative_id.country_code on an invoice recipient. Same code in both.
  • If country_code = ES, then type must be PASSPORT (03) or NOT_REGISTERED (07) (ALTERNATIVE_ID_SPAIN_INVALID_TYPE).
  • If type = NOT_REGISTERED (07), then country_code must be ES (ALTERNATIVE_ID_REQUIRES_SPAIN), and number must be a Spanish DNI or NIE: a Spanish company is always registered (RECIPIENT_UNREGISTERED_ID_MUST_BE_DNI_OR_NIE). That DNI or NIE must not be one the AEAT census identifies: if it is, the identifier is rejected with ALTERNATIVE_ID_ACTUALLY_IN_CENSUS and has to be sent as nif. A customer is checked when it is saved; an invoice recipient, when the invoice is issued with VeriFactu. If the census does not answer, the identifier is not rejected.
  • If type = NIF_IVA (02), country_code must be an EU member state other than Spain (ALTERNATIVE_ID_VAT_REQUIRES_EU_COUNTRY), and number must have that country's EU VAT number structure as AEAT defines it: the country prefix (EL for Greece) followed by the national number, e.g. FR40303265045, DE123456789, EL094014201 (ALTERNATIVE_ID_VAT_INVALID_FORMAT). Lowercase letters are accepted and stored in uppercase. A customer from outside the EU is identified with another type, such as OTHER_DOCUMENT or COUNTRY_ID.

A number that is blank once trimmed is rejected with ALTERNATIVE_ID_INVALID.

Well-formed is not the same as registered: an EU VAT number that is not in the VIES census is still rejected by VeriFactu after the invoice is issued.

An identifier returned in a response is the one stored. A customer saved before a rule existed keeps its identifier and can still be read; issuing an invoice to it with an identifier that breaks these rules is rejected with the same code, before a number is used.

Matrix of allowed combinations

typecountry_code = EScountry_code ≠ ES
NIF_IVA (02)✗EU member states only
PASSPORT (03)✓✓
COUNTRY_ID (04)✗✓
RESIDENCE_CERTIFICATE (05)✗✓
OTHER_DOCUMENT (06)✗✓
NOT_REGISTERED (07)✓✗
address

Address you send when you create or update a company, a customer or an onboarding.

A required field made only of whitespace passes the length checks below but leaves the address without that part, so it is rejected with 422: STREET_EMPTY, POSTAL_CODE_EMPTY, CITY_EMPTY or PROVINCE_EMPTY.

Addresses you read back are described by their own schema.

phone?string

A phone number as this API accepts it: 9 to 20 characters, and only digits, spaces, dashes, parentheses and an optional leading +. Every request that takes a phone number uses this schema.

It is Phone plus the rules enforced on input. A value this schema accepts always satisfies Phone, so anything you send here is something a response can return.

Match^[+]?[0-9\s\-\(\)]+$
Length9 <= length <= 20
email?string|null

Main contact address (minimum valid email is 5 chars, e.g. a@b.co). Invoice emails go here only when the customer has no billing_emails.

Formatemail
Length5 <= length <= 255
website?string|null

Website URL

Match^(https?://.+|)$
Lengthlength <= 255
billing_emails?|null

Addresses that receive the customer's invoice emails. When the send request names no recipients and the invoice has no email_config recipients, invoices go to these addresses, and not to email; email is used only when billing_emails is empty.

contact_person?string|null

Contact person name (optional)

Lengthlength <= 200
notes?string|null

Additional notes about the customer (optional)

preferred_payment_method?
general_discount?number|null

General discount percentage (optional)

Range0 <= value <= 100

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://app.beel.es/api/v1/companies/497f6eca-6276-4993-bfeb-53cbbbba6f08/customers" \  -H "Content-Type: application/json" \  -d '{    "legal_name": "Tech Solutions SL",    "nif": "B12345674",    "email": "admin@techsolutions.com",    "phone": "+34912345678",    "address": {      "street": "Calle Mayor",      "number": "123",      "floor": "2º B",      "postal_code": "28013",      "city": "Madrid",      "province": "Madrid",      "country": "España"    },    "notes": "Cliente prioritario - Pago puntual"  }'

Customer created successfully with generated ID and initial statistics

{
  "success": true,
  "data": {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "legal_name": "Tech Solutions SL",
    "nif": "B12345674",
    "email": "admin@techsolutions.com",
    "phone": "+34912345678",
    "address": {
      "street": "Calle Mayor",
      "number": "123",
      "floor": "2º B",
      "postal_code": "28013",
      "city": "Madrid",
      "province": "Madrid",
      "country": "España"
    },
    "notes": "Cliente prioritario - Pago puntual",
    "active": true,
    "created_at": "2025-01-20T10:00:00Z",
    "updated_at": "2025-01-20T10:00:00Z"
  },
  "meta": {
    "timestamp": "2025-01-20T10:00:00Z",
    "request_id": "c1c1c1c1-0001-4000-a000-000000000001"
  }
}
{
  "success": false,
  "error": {
    "code": "INVALID_JSON_FORMAT",
    "message": "The field 'due_date' has an invalid date format: '2026-03-04fds'. Expected format: YYYY-MM-DD.",
    "details": {
      "field": "due_date",
      "invalid_value": "2026-03-04fds",
      "expected_format": "YYYY-MM-DD"
    }
  },
  "meta": {
    "timestamp": "2026-03-05T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication is required to access this resource"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The provided data is not valid",
    "details": {
      "field": "specific error message"
    }
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  },
  "type": "https://docs.beel.es/errors/INVOICE_NO_LINES",
  "title": "INVOICE_NO_LINES",
  "detail": "The invoice must have at least one line",
  "instance": "/v1/invoices/abc-123"
}

An equivalent resource already exists. error.code names the clash (here a customer with the same identifier); details is an empty object.

{
  "success": false,
  "error": {
    "code": "CLIENT_DUPLICATE",
    "message": "A client with this NIF already exists",
    "details": {}
  },
  "meta": {
    "timestamp": "2025-01-20T10:00:00Z",
    "request_id": "d4d4d4d4-0004-4000-a000-000000000004"
  }
}

The body parses but one or more values are not acceptable. details is a flat map from the offending property (snake_case, nested paths joined with a dot) to a message; one entry per property.

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The provided data is not valid.",
    "details": {
      "nif": "The field 'nif' cannot be empty",
      "lines": "The field 'lines' cannot be empty",
      "recipient.address.postal_code": "Contains invalid characters."
    }
  },
  "meta": {
    "timestamp": "2025-01-20T10:00:00Z",
    "request_id": "c3c3c3c3-0003-4000-a000-000000000003"
  }
}
{
  "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"
  }
}
{
  "success": false,
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Internal server error"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
{
  "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"
  }
}