# Create an invitation API Reference

Creates a single-use invitation for a person to join the account with the given
`account_role`.

- **`token`:** the acceptance secret, returned once and never readable again, so deliver
  it to the invitee. `invitation_url` is the ready-to-use link built from that same token.
- **`grants`:** the companies a `MEMBER` starts with. Omit it, or send `[]`, to invite
  them with no company access yet; an explicit `null` is rejected with `422`. Grants are only valid for `MEMBER`, since `OWNER` and
  `ADMIN` reach every company implicitly.
- **`account_role`:** `OWNER` cannot be invited. An account has exactly one owner, handed
  over only through `PUT /v1/accounts/{account_id}/owner`.
- **`send_email`:** defaults to `false`, so BeeL sends no email and you deliver the token
  or `invitation_url` yourself. Set it to `true` to have the invitation emailed to
  `invited_email` as well.


## POST /v1/accounts/{account_id}/invitations

**Create an invitation**

Creates a single-use invitation for a person to join the account with the given
`account_role`.

- **`token`:** the acceptance secret, returned once and never readable again, so deliver
  it to the invitee. `invitation_url` is the ready-to-use link built from that same token.
- **`grants`:** the companies a `MEMBER` starts with. Omit it, or send `[]`, to invite
  them with no company access yet; an explicit `null` is rejected with `422`. Grants are only valid for `MEMBER`, since `OWNER` and
  `ADMIN` reach every company implicitly.
- **`account_role`:** `OWNER` cannot be invited. An account has exactly one owner, handed
  over only through `PUT /v1/accounts/{account_id}/owner`.
- **`send_email`:** defaults to `false`, so BeeL sends no email and you deliver the token
  or `invitation_url` yourself. Set it to `true` to have the invitation emailed to
  `invited_email` as well.

### Authentication

Accepts any of:

- `ApiKeyAuth` (HTTP bearer, token format `beel_sk_*`)

### Parameters

- **account_id** (required) in path `string`: Your own account, or an account you provisioned. It — not the credential — decides which account the operation acts on; a `403` is returned when you do not reach it, the same response an account that does not exist gets.
- **Idempotency-Key** (optional) in header `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. | Status | Code | When | |---|---|---| | `400` | `INVALID_IDEMPOTENCY_KEY` | The key breaks the format rules above. | | `409` | `IDEMPOTENCY_KEY_PROCESSING` | The first request is still in flight. Wait for the `Retry-After` seconds (2) and retry with the same key. | | `409` | `IDEMPOTENCY_KEY_MISMATCH` | The key was already used with a **different** body. Use a new key. |

### Request Body

Required.

**Content `application/json`:**

- **invited_email** (required) `string` (email): Email address of the invited person.
- **account_role** (required) `AccountRole`: Who administers the account. Independent of `access_level`, which says how much access someone has to a given company. `OWNER` — full control, including billing, API keys and transferring ownership. Exactly one per account, so it is never an accepted value when you SET a role (inviting a member or changing one's role): both reject it with `422 OWNER_ROLE_NOT_ASSIGNABLE`. Ownership moves only through `PUT /v1/accounts/{account_id}/owner`. `ADMIN` — everything an `OWNER` can do, except transferring ownership. `MEMBER` — no account administration. Access to each company is granted individually and reported as `access_level`; a member only sees the companies granted to them.
- **send_email** `boolean`: If `true`, an invitation email with the acceptance link is sent to `invited_email` in addition to returning the token. Defaults to `false` (you deliver the token/link yourself).
- **grants** `array[GrantAssignment]`: Initial grants (only when `account_role` is `MEMBER`). Omit it, or send `[]`, to invite with no company access yet (granted later). An explicit `null` is rejected with `422` `VALIDATION_ERROR`.

**Example `create_invitation_member`** — Invite a member with access to one company:

```json
{
  "invited_email": "ana.garcia@example.com",
  "account_role": "MEMBER",
  "send_email": true,
  "grants": [
    {
      "company_id": "550e8400-e29b-41d4-a716-446655440000",
      "access_level": "OPERATE"
    }
  ]
}
```

**Example `create_invitation_admin`** — Invite an admin:

```json
{
  "invited_email": "luis.martin@example.com",
  "account_role": "ADMIN",
  "send_email": false,
  "grants": []
}
```

### Responses

#### 201: Invitation created successfully

**Headers:**

- `Location` `string`: URI of the created resource — its canonical GET (`/v1/companies/{company_id}/...` or `/v1/accounts/{account_id}/...`).

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: true)
- **data** (required): The payload. An object for a single resource; an object holding the named collection (and its `pagination`) for a listing. Never a bare array at this level in v1.
- **meta** `ResponseMeta`
- **data** `Invitation`: A created invitation. `token` is the single-use acceptance secret, returned only once (never exposed again); deliver it to the invitee. `invitation_url` is the ready-to-deliver acceptance link BeeL builds for the current environment from that same token.

#### 400: Bad request

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): 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** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: 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** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "BAD_REQUEST",
    "message": "Invalid request"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 401: 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`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): 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** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: 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** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication is required to access this resource"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 403: Every cause listed under the plain `403` above, plus the two that govern a change to the people of an account: `MEMBER_MANAGEMENT_FORBIDDEN`, when your role in the account does not manage its members, invitations or grants (only an `OWNER` or an `ADMIN` does), and `LIVE_CREDENTIAL_REQUIRED`, when the call is made with a test API key (`beel_sk_test_…`): members, invitations and grants are shared between Test and Live, so changing them is always a real change. Use your live API key or the dashboard.

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): 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** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: 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** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "LIVE_CREDENTIAL_REQUIRED",
    "message": "This operation changes the real account; it requires a live API key or a dashboard session."
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 409: `MEMBER_ALREADY_IN_ACCOUNT` — the invited person already belongs to this account, so there is nothing to invite. Change their role with `PATCH /v1/accounts/{account_id}/members/{member_id}` instead.

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): 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** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: 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** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "MEMBER_ALREADY_IN_ACCOUNT",
    "message": "That person already belongs to this account."
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 422: Validation error. `error.code` is `OWNER_ROLE_NOT_ASSIGNABLE` (`account_role` was `OWNER`; returned to every caller, owners included), `GRANTS_ONLY_FOR_MEMBER` (grants provided with `account_role` other than `MEMBER`) or `GRANT_COMPANY_NOT_IN_ACCOUNT` (a grant's company does not belong to the account).

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): 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** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: 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** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

#### 429: Rate limit exceeded

**Headers:**

- `Retry-After` `integer`: Seconds until the rate limit resets
- `RateLimit-Limit` `integer`: Maximum requests allowed in the window
- `RateLimit-Remaining` `integer`: Remaining requests in the current window
- `RateLimit-Reset` `integer`: Seconds until the current window resets

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): 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** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: 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** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "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"
  }
}
```

#### 500: Internal server error

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): 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** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: 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** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Internal server error"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### default: 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 `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`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): 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** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: 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** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "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"
  }
}
```

---

# Related Schema Definitions

## CreateInvitationRequest

Invites a person to join the active account.

- **invited_email** (required) `string` (email): Email address of the invited person.
- **account_role** (required) `AccountRole`: Who administers the account. Independent of `access_level`, which says how much access someone has to a given company. `OWNER` — full control, including billing, API keys and transferring ownership. Exactly one per account, so it is never an accepted value when you SET a role (inviting a member or changing one's role): both reject it with `422 OWNER_ROLE_NOT_ASSIGNABLE`. Ownership moves only through `PUT /v1/accounts/{account_id}/owner`. `ADMIN` — everything an `OWNER` can do, except transferring ownership. `MEMBER` — no account administration. Access to each company is granted individually and reported as `access_level`; a member only sees the companies granted to them.
- **send_email** `boolean`: If `true`, an invitation email with the acceptance link is sent to `invited_email` in addition to returning the token. Defaults to `false` (you deliver the token/link yourself).
- **grants** `array[GrantAssignment]`: Initial grants (only when `account_role` is `MEMBER`). Omit it, or send `[]`, to invite with no company access yet (granted later). An explicit `null` is rejected with `422` `VALIDATION_ERROR`.

## AccountRole

Who administers the account. Independent of `access_level`, which says how much access someone has to a given company.

`OWNER` — full control, including billing, API keys and transferring ownership. Exactly one per account, so it is never an accepted value when you SET a role (inviting a member or changing one's role): both reject it with `422 OWNER_ROLE_NOT_ASSIGNABLE`. Ownership moves only through `PUT /v1/accounts/{account_id}/owner`.
`ADMIN` — everything an `OWNER` can do, except transferring ownership.
`MEMBER` — no account administration. Access to each company is granted individually and reported as `access_level`; a member only sees the companies granted to them.

Type: `string` — one of: OWNER, ADMIN, MEMBER

## GrantAssignment

A member's access to a specific company.

- **company_id** (required) `string` (uuid): Unique identifier (UUID) of the company within the account.
- **access_level** (required) `string`: Access the member gets over this company. `NONE` is not accepted here: a grant that grants nothing is not a grant. Remove access by deleting the grant (`DELETE /v1/accounts/{account_id}/members/{member_id}/grants/{company_id}`). — one of: VIEW, OPERATE

## SuccessResponse

The envelope every successful JSON response of the BeeL. API is wrapped in. There are no
bare resources in v1 and none are planned: the payload always hangs off `data`.

- A **single resource** is an object in `data`.
- A **collection** hangs off a named key inside `data`, together with its `pagination`,
  also inside `data` — `data: {invoices: [...], pagination: {...}}`.
- A collection carries `pagination` unless its operation declares itself a **closed
  catalogue**: a fixed, bounded list with nothing to page through. The declaration is
  explicit in the operation; a missing `pagination` is never something to infer.
- `GET /v1/accounts` pages by cursor (`data: {accounts: [...], next_cursor}`). It is a
  documented variant of pagination, not another envelope.

Putting the array straight into `data` with `pagination` as its sibling is the shape a v2
would adopt; v1 is not being flipped to it.

- **success** (required) `boolean`: No description (example: true)
- **data** (required): The payload. An object for a single resource; an object holding the named collection (and its `pagination`) for a listing. Never a bare array at this level in v1.
- **meta** `ResponseMeta`

## ResponseMeta

- **timestamp** `string` (date-time): No description (example: "2025-01-15T10:30:00Z")
- **request_id** `string`: No description (example: "4bf92f3577b34da6a3ce929d0e0e4736")

## Invitation

A created invitation. `token` is the single-use acceptance secret, returned only once (never exposed again); deliver it to the invitee. `invitation_url` is the ready-to-deliver acceptance link BeeL builds for the current environment from that same token.

- **invitation_id** (required) `string` (uuid): No description
- **invited_email** (required) `string` (email): No description
- **account_role** (required) `AccountRole`: Who administers the account. Independent of `access_level`, which says how much access someone has to a given company. `OWNER` — full control, including billing, API keys and transferring ownership. Exactly one per account, so it is never an accepted value when you SET a role (inviting a member or changing one's role): both reject it with `422 OWNER_ROLE_NOT_ASSIGNABLE`. Ownership moves only through `PUT /v1/accounts/{account_id}/owner`. `ADMIN` — everything an `OWNER` can do, except transferring ownership. `MEMBER` — no account administration. Access to each company is granted individually and reported as `access_level`; a member only sees the companies granted to them.
- **expires_at** (required) `string` (date-time): No description
- **token** (required) `string`: Single-use acceptance token (shown once); the source of truth of the invitation.
- **invitation_url** `string` (uri): Ready-to-use acceptance link for the invitee, built by BeeL for the current environment (no need to assemble `/aceptar?token=` yourself). Derived from `token`, which remains the single-use source of truth. `null` only if no token was issued.

## ErrorResponse

Error response shared by all BeeL. APIs.

The payload carries **two contracts at once** (additive, non-breaking):

- **Legacy** (`success`, `error.{code,message,details}`, `meta`) — kept
  intact for existing consumers.
- **RFC 9457** (`type`, `title`, `detail`, `instance`) — new fields
  for integrators following Problem Details for HTTP APIs. The
  `type` URI is the stable, shareable link to the error's
  documentation page (e.g. `https://docs.beel.es/errors/{code}`).

Future migration: the legacy fields will be deprecated via
`Deprecation`/`Sunset` headers after a sufficient adoption window,
and the response Content-Type will move to
`application/problem+json`.

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): 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** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: 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** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

## ErrorDetail

- **code** (required) `string`: No description (example: "VALIDATION_ERROR")
- **message** (required) `string`: No description (example: "The provided data is not valid")
- **details** `object`: No description (example: {"field":"specific error message"})


---

Full OpenAPI spec: https://docs.beel.es/api/openapi