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

# API introduction

> REST API for programmatic access to your Vorel tenant. Bearer-token auth, JSON requests + responses, full OpenAPI 3.1 spec.

The Vorel public API is a REST surface at `https://app.vorel.ai/api/v1/*`. It's the same surface our [`@vorel/sdk`](/api-reference/sdks) TypeScript client uses, the same surface n8n around-the-brain workflows use, and the same surface your custom integrations should target.

## Quickstart

<Steps>
  <Step title="Generate an API key">
    Sign in at `app.vorel.ai`, navigate to **Settings → Integrations → API keys**, click **Create API key**. Pick the scopes your integration needs: `read`, `leads:write`, `appointments:write`, `offerings:write`, `conversations:write`, `crm:write`. Copy the key; it's shown only once (scrypt-hashed in storage).

    The key format is `vapk_live_<48 hex chars>` (58 chars total). See [Authentication](/api-reference/authentication) for the full lifecycle.
  </Step>

  <Step title="Make your first request">
    ```bash theme={null}
    curl https://app.vorel.ai/api/v1/conversations \
      -H "Authorization: Bearer vapk_live_..." \
      -H "Content-Type: application/json"
    ```

    Returns a paginated list of your tenant's conversations.
  </Step>

  <Step title="Browse the full reference">
    The API Reference tab in this docs site auto-renders from our [live OpenAPI spec](https://app.vorel.ai/api/v1/openapi.json): every endpoint, every parameter, every response shape, with interactive "try it" examples per route.
  </Step>
</Steps>

## What the API exposes

<CardGroup cols={2}>
  <Card title="Conversations" icon="messages">
    Read inbound conversations across voice + chat. List, get-by-id, create (pre-create from your
    CRM webhook), update.
  </Card>

  <Card title="Leads" icon="user-tag">
    Read + write leads. Update qualification state, attach attributes, trigger handoff, mark stale.
  </Card>

  <Card title="Appointments" icon="calendar">
    Read + write appointments. Schedule, reschedule, cancel, complete. Lifecycle webhooks emit on
    every transition.
  </Card>

  <Card title="Offerings" icon="store">
    Read + write your catalog (properties / services / treatments / menu items, vertical-specific).
    Soft-delete supported.
  </Card>

  <Card title="Analytics" icon="chart-line">
    `GET /v1/analytics/weekly-rollup`: aggregate metrics for the prior N days. Powers
    around-the-brain digest workflows.
  </Card>

  <Card title="CRM proxy" icon="database">
    `POST /v1/crm/create-record`: write into your tenant's connected CRM via Vorel's per-tenant
    driver layer. Same path the agent uses.
  </Card>
</CardGroup>

For the complete endpoint catalog, see the **API Reference** tab.

## Conventions

### Auth

All endpoints require `Authorization: Bearer <api_key>`. Keys are tenant-scoped; one key only sees its own tenant's data, enforced via Postgres RLS.

Endpoints that mutate state require a write scope (`leads:write`, `appointments:write`, `offerings:write`, `conversations:write`). Endpoints that only read accept the `read` scope. A key without the matching scope returns a `403 forbidden` envelope.

### Response shapes

Successful responses are JSON. List endpoints use cursor pagination:

```json theme={null}
{
  "data": [
    /* array of resources */
  ],
  "next_cursor": "uuid-or-null",
  "has_more": false
}
```

`next_cursor` and `has_more` are top-level fields alongside `data` (not nested under a `pagination` object). Pass `next_cursor` back as `?cursor=<uuid>` to fetch the next page; `has_more` is `false` and `next_cursor` is `null` when you've reached the end.

Single-resource endpoints return the resource directly.

### Error envelope

Every error returns a consistent shape:

```json theme={null}
{
  "error": {
    "code": "rate_limited",
    "message": "Per-key rate limit exceeded; retry after 60s."
  }
}
```

Codes: `unauthorized`, `forbidden`, `rate_limited`, `bad_request`, `not_found`, `conflict`, `internal_error`. The HTTP status code matches the semantic.

### Idempotency

`POST /v1/crm/create-record` accepts an `idempotency_key` field in the request body. The value is forwarded into the per-tenant CRM driver so the underlying CRM can dedupe its own write. Other write endpoints don't currently accept an idempotency key; safe-retry semantics there rely on the resource's natural unique constraints (e.g. `(tenant_id, channel, customer_identifier)` for conversations).

### Webhooks

The API also emits **outbound webhooks** to URLs you register at `/(dashboard)/settings/integrations/webhooks`. 12 event types: `lead.created`, `lead.updated`, `lead.qualified`, `conversation.created`, `conversation.handoff_requested`, `conversation.closed`, `booking.created`, `booking.rescheduled`, `booking.cancelled`, `booking.completed`, `offering.created`, `offering.updated`. Bodies are HMAC-SHA256 signed via `X-Webhook-Signature`; the envelope `id` field doubles as the consumer's idempotency key.

See [Webhooks](/api-reference/webhooks) for the full spec.

## Rate limits

API requests are gated by a layered rate-limit stack. The most-restrictive applicable layer wins:

| Layer                | Limit        | Triggers when                                                                  |
| -------------------- | ------------ | ------------------------------------------------------------------------------ |
| Per-API-key          | 200 req/min  | Each issued `vapk_*` key has its own bucket (`apk:<key_id>`)                   |
| Per-tenant aggregate | 5000 req/min | Total across every authenticated surface in your tenant                        |
| Per-(tenant, tool)   | 50 req/min   | Each internal tool endpoint (e.g. `book_appointment`) consumed by the agent    |
| Per-dashboard-user   | 200 req/min  | Authenticated dashboard sessions (separate from API keys)                      |
| Per-IP webhook       | 500 req/min  | Inbound calls to `/api/webhooks/*` (WhatsApp / voice / auth webhook receivers) |

The limiter is fail-open: a Redis blip admits the request rather than 429-ing every caller. Hitting a limit returns `429 rate_limited` with `Retry-After` + `X-RateLimit-Limit` + `X-RateLimit-Remaining` + `X-RateLimit-Reset` headers. See [Rate limits](/api-reference/rate-limits) for the detailed stack.

## SDKs

<CardGroup cols={2}>
  <Card title="TypeScript / JavaScript" icon="js" href="/api-reference/sdks">
    `@vorel/sdk` is a full-coverage, typed client with zero runtime deps. Today the REST API plus
    the live OpenAPI spec are the supported integration path; a published `@vorel/sdk` package is
    on the roadmap.
  </Card>

  <Card title="OpenAPI (any language)" href="https://app.vorel.ai/api/v1/openapi.json">
    Live OpenAPI 3.1 spec. Import into Postman / Insomnia / Bruno, or generate a client in any
    language via `openapi-generator` / `oazapfts` / etc.
  </Card>
</CardGroup>

## Common integration patterns

<AccordionGroup>
  <Accordion icon="bolt" title="Push a lead from your website form">
    Call `POST /v1/conversations` to pre-create the conversation from your form's
    customer-identifier (phone/email), then `POST /v1/leads` to attach the qualification data. The
    next time the customer calls or WhatsApps, Vorel matches them to the existing conversation by
    `(channel, customer_identifier)`. No fragmentation.
  </Accordion>

  <Accordion icon="bell" title="Get notified when a new lead qualifies">
    Subscribe to the `lead.qualified` webhook event. Vorel POSTs to your URL with the lead row + the
    conversation's last 10 messages. HMAC-signed; verify the signature before acting.
  </Accordion>

  <Accordion icon="calendar-clock" title="Send a 24h booking reminder">
    Use the [n8n post-booking-confirmation template](/integrations/n8n): wires a `booking.created`
    webhook to a delay node + `POST /v1/conversations/{id}/send` for the reminder.
  </Accordion>

  <Accordion icon="filter" title="Daily lead-nurture cadence">
    Use the [n8n lead-nurture-3-day template](/integrations/n8n): daily cron + `GET
            /v1/leads?stale_for_days=3` + `POST /v1/conversations/{id}/send` for re-engagement. The
    `stale_for_days` filter already scopes to qualified leads whose conversation has gone quiet
    for more than N days, so no separate `status` filter is needed (and none is supported today).
  </Accordion>

  <Accordion icon="chart-bar" title="Weekly analytics digest">
    Use the [n8n weekly-qa-rollup template](/integrations/n8n): Monday cron + `GET
            /v1/analytics/weekly-rollup` + format + send via an email provider or Slack.
  </Accordion>
</AccordionGroup>

## Status

The API is live and stable. All `v1/*` endpoints have stable contracts; we'll version-bump (`/v2`) before any breaking change.

Public status page: [`app.vorel.ai/status`](https://app.vorel.ai/status). Expect \~99.9% on the API surface (per [our SLOs](https://docs.vorel.ai/security/overview#slos)).

{/* verified-against: apps/web/src/lib/rate-limit.ts rateLimitTenantTotal (5000), rateLimitWebhookByIp (500), per-(tenant,tool) 50 (handoff/docs/14-security.md) */}

{/* verified-against: apps/web/src/lib/api-key.ts key format `vapk_live_<48 hex>` (58 chars) + scrypt hash at rest (N=2^14, r=8, p=1) */}

{/* verified-against: apps/web/src/app/api/v1/crm/create-record/route.ts idempotency_key body field (only POST that takes one today) */}
