> ## Documentation Index
> Fetch the complete documentation index at: https://docs.recurso.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect an Integration

> Store the workspace's own credentials for a tax, CRM, or storage provider, sealed at rest; becomes the category's single active connection.

Stores the workspace's own credentials for one tax, CRM, or storage provider.
The config is sealed as a single encrypted blob; secrets are write-only and
never returned. Each call inserts a new connection row with a fresh `id` and
deactivates every active connection in the same `category` first — not only
the same provider. A workspace therefore has at most one active provider per
category: connecting `ziptax` disconnects an active `taxjar` or `avalara`, and
reconnecting the same provider to rotate credentials returns a new `id` with
`created_at` equal to `updated_at`. Do not persist a connection `id` and expect
it to survive a reconnect; the deactivated rows are kept but never listed.
Owner/admin only — API-key callers are not role-gated. Check `vault_ready` on
[List Integration Connections](/api-reference/integration-connections/list)
first; once a HubSpot connection exists, verify it with
[Sync CRM Now](/api-reference/integration-connections/crm-sync).

## Request Body

| Parameter  | Type                     | Required | Description                                                                                                                                                                                                                                                                                                                                 |
| ---------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `category` | string                   | Yes      | `tax`, `crm`, or `storage`                                                                                                                                                                                                                                                                                                                  |
| `provider` | string                   | Yes      | Must match the category: `tax` → `taxjar`, `avalara`, `ziptax`; `crm` → `hubspot`; `storage` → `s3`                                                                                                                                                                                                                                         |
| `config`   | object (string → string) | Yes      | Provider config. Values are trimmed. Required keys per provider: `taxjar` and `ziptax` need `api_key`; `avalara` needs `account_id`, `license_key`, `company_code`; `hubspot` needs `access_token`; `s3` needs `bucket`, `region`, `access_key_id`, `secret_access_key`, plus an optional `endpoint` for S3-compatible stores such as MinIO |

Any `endpoint`, `api_url`, or `base_url` value is validated before storage. It
must always parse as an `http` or `https` URL. On the hosted (multi-tenant)
deployment it must additionally use `https` and its host must resolve to a
public address — loopback, link-local, private, and other reserved ranges are
rejected. Self-hosted deployments skip the `https` and address checks.

## Example Request

```bash theme={null}
curl -X POST "https://api.recurso.dev/v1/integration-connections" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "category": "storage",
    "provider": "s3",
    "config": {
      "bucket": "acme-invoices",
      "region": "ap-south-1",
      "access_key_id": "AKIAIOSFODNN7EXAMPLE",
      "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
    }
  }'
```

## Response

Returns `201 Created` with the secret-free projection of the new connection.
Because a connect always inserts a new row, `created_at` and `updated_at` are
the same instant and the `id` differs from any previous connection for the
same provider.

```json theme={null}
{
  "data": {
    "id": "c7d8e9f0-1a2b-4c3d-9e4f-5a6b7c8d9e0f",
    "category": "storage",
    "provider": "s3",
    "config": {
      "bucket": "acme-invoices",
      "region": "ap-south-1"
    },
    "has_secrets": true,
    "created_at": "2026-08-30T08:11:44Z",
    "updated_at": "2026-08-30T08:11:44Z"
  }
}
```

## Fields

| Field         | Type               | Description                                                                                              |
| ------------- | ------------------ | -------------------------------------------------------------------------------------------------------- |
| `id`          | string (uuid)      | The connection's id — new on every connect, including a credential rotation for the same provider        |
| `category`    | string             | `tax`, `crm`, or `storage`                                                                               |
| `provider`    | string             | The provider that was connected                                                                          |
| `config`      | object             | Non-secret config only (`api_url`, `base_url`, `endpoint`, `region`, `bucket`, `prefix`, `company_code`) |
| `has_secrets` | boolean            | `true` when at least one secret value was stored                                                         |
| `created_at`  | string (date-time) | When this connection row was created                                                                     |
| `updated_at`  | string (date-time) | Same as `created_at` on the response — the row is new; it only moves later when the row is deactivated   |

## Errors

| Status | Code                | When                                                                                                                                                                             | Fix                                                               |
| ------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `400`  | `validation_failed` | `category`, `provider`, or `config` missing from the body, or the body is not valid JSON                                                                                         | Send all three fields                                             |
| `400`  | `validation_failed` | `(category, provider)` is not a supported pair                                                                                                                                   | Use one of the pairs listed under Request Body                    |
| `400`  | `validation_failed` | A required config key for the provider is empty (message names it, e.g. `hubspot: access_token is required`)                                                                     | Add the missing key                                               |
| `400`  | `validation_failed` | An `endpoint`/`api_url`/`base_url` is not an `http(s)` URL; on the hosted deployment also when it is not `https`, does not resolve, or resolves to a private or reserved address | Use the provider's public HTTPS endpoint                          |
| `401`  | `unauthorized`      | Missing or invalid API key / session                                                                                                                                             | Send `Authorization: Bearer $API_KEY`                             |
| `403`  | `forbidden`         | The session user is not an owner or admin                                                                                                                                        | Integration credentials are workspace config — ask an owner/admin |
| `500`  | `internal_error`    | Persisting or sealing the connection failed                                                                                                                                      | Retry; contact support if it persists                             |
| `503`  | `internal_error`    | Credential vault unavailable (`GATEWAY_ENCRYPTION_KEY` not configured on the API)                                                                                                | Self-hosted: set `GATEWAY_ENCRYPTION_KEY` and restart             |

Errors use the standard envelope — see [Errors](/api-reference/errors).


## OpenAPI

````yaml POST /v1/integration-connections
openapi: 3.1.0
info:
  title: Recurso API
  version: 1.0.0
  description: |
    The Recurso billing engine REST API.

    Authenticate by passing your API key as a bearer token:

        Authorization: Bearer <api_key>

    Obtain an API key by registering a tenant via `POST /auth/register`.
    All authenticated endpoints live under the `/v1` prefix. Mutating
    endpoints support idempotency via the `Idempotency-Key` header.
  license:
    name: MIT
    identifier: MIT
servers:
  - url: https://billing.example.com
    description: >-
      Example deployment — substitute the base URL of your own Recurso
      deployment.
security:
  - bearerAuth: []
tags:
  - name: System
    description: Health, version, and API metadata
  - name: Auth
    description: Tenant registration
  - name: Plans
    description: Product catalog plans
  - name: Customers
    description: Customer management
  - name: Subscriptions
    description: Subscription lifecycle
  - name: Invoices
    description: Invoices, PDFs, and Indian GST e-invoicing
  - name: Coupons
    description: Discounts
  - name: Usage
    description: Metered usage events
  - name: Credit Notes
    description: Customer credits
  - name: Quotes
    description: Quote-to-invoice lifecycle
  - name: Webhooks
    description: Webhook endpoint management and event feed
  - name: Analytics
    description: Revenue analytics
  - name: Checkout
    description: Public hosted checkout for invoices
  - name: Payments
    description: Payment order creation
  - name: Inbound Webhooks
    description: Receivers for payment-gateway callbacks (Razorpay, Stripe)
  - name: Customer Portal
    description: Customer-facing portal — magic-link auth and session-scoped data
  - name: Developer
    description: API key management
  - name: Account
    description: Tenant account settings
  - name: Finance
    description: Ledger accounts, entries, reconciliation, and revenue recognition
  - name: Settings
    description: Tax (GST) and e-invoicing (IRP) configuration
  - name: Consents
    description: Consent records for RBI-compliant recurring billing
  - name: Referrals
    description: Customer referral program
  - name: Gifts
    description: Gift subscriptions
  - name: Mandates
    description: UPI Autopay mandates
  - name: Offline Payments
    description: Virtual accounts and manually recorded payments
  - name: Organizations
    description: Multi-entity organizations grouping several tenants
  - name: Accounting
    description: QuickBooks / Xero accounting integrations
  - name: Churn
    description: Churn risk scoring and alerts
  - name: Cancel Flows
    description: Configurable retention flows shown at cancellation time
  - name: Dunning
    description: Dunning analytics and multi-channel dunning campaigns
paths:
  /v1/integration-connections:
    post:
      tags:
        - Integration Connections
      summary: Connect (or replace) a tax/CRM/storage integration
      description: |
        Stores the tenant's own integration credentials, sealed at rest. Secrets
        are write-only. Replaces any existing active connection for the
        (category, provider). Owner/admin only.
      operationId: createIntegrationConnection
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - category
                - provider
                - config
              properties:
                category:
                  type: string
                  enum:
                    - tax
                    - crm
                    - storage
                provider:
                  type: string
                  description: taxjar / avalara / ziptax / hubspot / s3.
                config:
                  type: object
                  additionalProperties:
                    type: string
                  description: >-
                    Provider config (e.g. api_key; or bucket/region/keys for
                    s3).
      responses:
        '201':
          description: Connected.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/IntegrationConnectionView'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '503':
          description: Credential vault unavailable (GATEWAY_ENCRYPTION_KEY not set).
components:
  schemas:
    IntegrationConnectionView:
      type: object
      description: Secret-free projection of a BYO tax/CRM/storage connection.
      properties:
        id:
          type: string
          format: uuid
        category:
          type: string
          enum:
            - tax
            - crm
            - storage
        provider:
          type: string
        config:
          type: object
          additionalProperties:
            type: string
          description: Non-secret config fields only (e.g. region, bucket, endpoints).
        has_secrets:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          description: Structured error detail.
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Stable machine-readable error code.
              examples:
                - validation_failed
                - unauthorized
                - forbidden
                - not_found
                - conflict
                - rate_limited
                - internal_error
                - invalid_api_key
                - key_mode_mismatch
                - over_refund
                - invoice_not_paid
                - invoice_already_paid
            message:
              type: string
              description: Human-readable explanation.
  responses:
    BadRequest:
      description: The request body or parameters are invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing or invalid credentials (API key or session cookie).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Authenticated but not permitted (insufficient role).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: >-
        Tenant API key obtained from `POST /auth/register` or `POST
        /v1/developer/keys`.

````