---
title: "Create an invitation"
method: POST
path: "/v1/accounts/{account_id}/invitations"
tags: ["Invitations"]
---

# Create an invitation

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

## Request

- Base URL: `https://app.beel.es/api`
- URL: `https://app.beel.es/api/v1/accounts/{account_id}/invitations`
- Auth: HTTP bearer

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.

## Headers

- `Idempotency-Key` string

## Request body

- CreateInvitationRequest: Invites a person to join the active account.
  - `invited_email` string, email, required: Email address of the invited person.
  - `account_role` 'OWNER' | 'ADMIN' | 'MEMBER', required: 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` 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`.
    - `company_id` string, uuid, required: Unique identifier (UUID) of the company within the account.
    - `access_level` 'VIEW' | 'OPERATE', required: 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}`).

## Response `201`

Invitation created successfully

- object: 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` boolean, required
  - `data` object, required: 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` string, uuid, required
    - `invited_email` string, email, required
    - `account_role` 'OWNER' | 'ADMIN' | 'MEMBER', required: 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` string, date-time, required
    - `token` string, required: Single-use acceptance token (shown once); the source of truth of the invitation.
    - `invitation_url` string, uri, nullable: 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.
  - `meta` ResponseMeta
    - `timestamp` string, date-time
    - `request_id` string

## Other responses

- `400`: Bad request. From a dashboard session, a `BeeL-Environment` header whose value is not a known environment answers `ENVIRONMENT_HEADER_INVALID`, with the header named in `error.details` (`{"BeeL-Environment": "invalid"}`); it is never read as production. `BUSINESS_RULE_VIOLATION` is the generic answer to an operation the resource does not admit in its current state when that state has no code of its own. It carries no `details`; read the resource again before retrying.
- `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.
- `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.
- `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.
- `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).
- `429`: 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.
- `500`: Internal server error
- `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 `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.

---

[API](https://skmtc.dev/beel/apis/beel-public-api.md) · [All operations](https://skmtc.dev/beel/apis/beel-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/beel/apis/beel-public-api/revisions/ffbc0ae31f79?raw)
