# Open an authorization to connect a payment provider API Reference

Opens an authorization session so the holder of a company your account **manages**
can connect a payment provider (`stripe`), and returns the `authorization_url` where they
authorize it.

- **`return_url`:** once the holder authorizes, BeeL's callback finalizes the connection
  and redirects back to the `return_url` of your portal, if you supplied one, with the
  parameters described under `return_url`.
- **When the connection appears:** it is created only when the holder authorizes, so it
  does not appear in `GET /v1/companies/{company_id}/payment-connections` until then. It
  is sealed under the NIF in the path, so auto-invoicing issues under that NIF.
- **The NIF must be activated in the mode of your API key** (`beel_sk_test_*` → Test,
  `beel_sk_live_*` → Live); otherwise the request answers `400`
  `COMPANY_NOT_ACTIVATED_IN_ENVIRONMENT` and no `authorization_url` is issued, because
  without activation there is no invoice series or tax configuration to invoice with.
  Test and Live activations are independent — a NIF activated in one mode still needs
  activating in the other.
- **One provider account, one NIF:** a provider account (`acct_...`) can be connected to a
  single NIF across the whole platform. Authorizing the same provider account from a second
  NIF does not move it: the callback fails with
  `OAUTH_ACCOUNT_CONNECTED_TO_OTHER_COMPANY`, and the existing connection keeps invoicing
  under the NIF it was sealed with. To move it, first
  `DELETE /v1/companies/{company_id}/payment-connections/{connection_id}` on the NIF
  that holds it, then open a new authorization on the NIF you want it under.


## POST /v1/companies/{company_id}/payment-connections/authorizations

**Open an authorization to connect a payment provider**

Opens an authorization session so the holder of a company your account **manages**
can connect a payment provider (`stripe`), and returns the `authorization_url` where they
authorize it.

- **`return_url`:** once the holder authorizes, BeeL's callback finalizes the connection
  and redirects back to the `return_url` of your portal, if you supplied one, with the
  parameters described under `return_url`.
- **When the connection appears:** it is created only when the holder authorizes, so it
  does not appear in `GET /v1/companies/{company_id}/payment-connections` until then. It
  is sealed under the NIF in the path, so auto-invoicing issues under that NIF.
- **The NIF must be activated in the mode of your API key** (`beel_sk_test_*` → Test,
  `beel_sk_live_*` → Live); otherwise the request answers `400`
  `COMPANY_NOT_ACTIVATED_IN_ENVIRONMENT` and no `authorization_url` is issued, because
  without activation there is no invoice series or tax configuration to invoice with.
  Test and Live activations are independent — a NIF activated in one mode still needs
  activating in the other.
- **One provider account, one NIF:** a provider account (`acct_...`) can be connected to a
  single NIF across the whole platform. Authorizing the same provider account from a second
  NIF does not move it: the callback fails with
  `OAUTH_ACCOUNT_CONNECTED_TO_OTHER_COMPANY`, and the existing connection keeps invoicing
  under the NIF it was sealed with. To move it, first
  `DELETE /v1/companies/{company_id}/payment-connections/{connection_id}` on the NIF
  that holds it, then open a new authorization on the NIF you want it under.

### Authentication

Accepts any of:

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

### Parameters

- **company_id** (required) in path `string`: Unique identifier (UUID) of the company the authorization is opened for — 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.
- **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`:**

- **provider** (required) `string`: Payment provider slug in **lowercase**. Currently only `stripe` (Stripe Connect) is operative; `woocommerce` and `shopify` are reserved for future providers. Any other value answers `422`. (example: "stripe")
- **return_url** `string`: URL of your portal to redirect the account holder back to after the OAuth callback completes. Must be an absolute `https://` URL. On **success** BeeL appends `status=success`, `provider` (slug), `company_id`, `connection_id` and `account` (the provider account id, e.g. `acct_...`). On **error** it appends `status=error`, `provider` and `message`, always a stable uppercase error code: `OAUTH_STATE_INVALID` (the authorization is unknown, expired or already used), `OAUTH_TOKEN_EXCHANGE_FAILED` (the provider rejected the code exchange), `ACCESS_DENIED` (the account holder declined at the provider), `OAUTH_ACCOUNT_CONNECTED_TO_OTHER_COMPANY` (the provider account is already connected to another NIF; disconnect it there first), `PROVIDER_ERROR` (any other provider-reported failure) or `OAUTH_UNEXPECTED`. When omitted, the callback redirects to BeeL's default integrations screen. A `return_url` that is not an absolute `https://` URL with a host is rejected up front with `422` `PAYMENT_RETURN_URL_INVALID`, and no authorization is opened. (example: "https://your-platform.example.com/connections/stripe/return")

**Example `initiate_stripe_connection`** — Connect a Stripe account:

```json
{
  "provider": "stripe",
  "return_url": "https://app.example.com/integrations/stripe/done"
}
```

### Responses

#### 200: Authorization URL generated

**Content `application/json`:**

- **success** `boolean`: No description
- **data** `object`: No description
  - **authorization_url** (required) `string` (uri): URL where the NIF's holder authorizes the Stripe Connect connection.
- **meta** `ResponseMeta`

#### 400: The NIF is not activated in the mode of your API key
(`COMPANY_NOT_ACTIVATED_IN_ENVIRONMENT`). Activate it in that mode and retry;
no authorization is opened and no authorization URL is issued.


**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")

#### 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: Your account does not own or manage this company. No authorization is opened.


**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")

#### 422: Validation or business rule error. One code is specific to this operation:

- `PROVIDER_NOT_SUPPORTED` — `provider` is not one BeeL connects to (today, only
  `stripe`). Nothing is opened.
- `VALIDATION_ERROR` — the generic validation failure, with the offending field in
  `error.details`.


**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

## InitiatePaymentConnectionRequest

The authorization to open: which provider it is for, and where to send the holder back.

- **provider** (required) `string`: Payment provider slug in **lowercase**. Currently only `stripe` (Stripe Connect) is operative; `woocommerce` and `shopify` are reserved for future providers. Any other value answers `422`. (example: "stripe")
- **return_url** `string`: URL of your portal to redirect the account holder back to after the OAuth callback completes. Must be an absolute `https://` URL. On **success** BeeL appends `status=success`, `provider` (slug), `company_id`, `connection_id` and `account` (the provider account id, e.g. `acct_...`). On **error** it appends `status=error`, `provider` and `message`, always a stable uppercase error code: `OAUTH_STATE_INVALID` (the authorization is unknown, expired or already used), `OAUTH_TOKEN_EXCHANGE_FAILED` (the provider rejected the code exchange), `ACCESS_DENIED` (the account holder declined at the provider), `OAUTH_ACCOUNT_CONNECTED_TO_OTHER_COMPANY` (the provider account is already connected to another NIF; disconnect it there first), `PROVIDER_ERROR` (any other provider-reported failure) or `OAUTH_UNEXPECTED`. When omitted, the callback redirects to BeeL's default integrations screen. A `return_url` that is not an absolute `https://` URL with a host is rejected up front with `422` `PAYMENT_RETURN_URL_INVALID`, and no authorization is opened. (example: "https://your-platform.example.com/connections/stripe/return")

## InitiatePaymentConnectionResponse

- **success** `boolean`: No description
- **data** `object`: No description
  - **authorization_url** (required) `string` (uri): URL where the NIF's holder authorizes the Stripe Connect connection.
- **meta** `ResponseMeta`

## ResponseMeta

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

## 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