# API Reference (/api-reference/overview)

## Base URL

```
https://bitelio-api.heltia.com
```

Every request goes to this base URL.

## Authentication

Pass your API key as a bearer token in the `Authorization` header:

```bash
Authorization: Bearer YOUR_API_KEY
```

* **Secret Key (sk\_\*)** — needed everywhere except `/v1/track`
* **Public Key (pk\_\*)** — accepted solely by `/v1/track`, so you can track events from client-side code

## Making requests

### Send transactional email

```bash
curl -X POST https://bitelio-api.heltia.com/v1/send \
  -H "Authorization: Bearer sk_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "user@example.com",
    "subject": "Hello",
    "body": "<p>Your message here</p>"
  }'
```

### Track event

```bash
curl -X POST https://bitelio-api.heltia.com/v1/track \
  -H "Authorization: Bearer pk_your_public_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "event": "signed_up"
  }'
```

### Create contact

```bash
curl -X POST https://bitelio-api.heltia.com/contacts \
  -H "Authorization: Bearer sk_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "subscribed": true,
    "data": {
      "firstName": "John",
      "plan": "pro"
    }
  }'
```

## Response format

### Success responses

The public endpoints (`/v1/send`, `/v1/track`, `/v1/verify`) wrap their payload in an envelope:

```json
{
  "success": true,
  "data": {
    "contact": "cnt_abc123",
    "event": "evt_xyz789",
    "timestamp": "2025-11-30T10:30:00.000Z"
  }
}
```

Dashboard-style endpoints (contacts, templates, campaigns, segments, workflows, and so on) skip the `success`/`data` envelope and hand back the resource itself:

```json
{
  "id": "cnt_abc123",
  "email": "user@example.com",
  "createdAt": "2025-11-30T10:30:00.000Z"
}
```

Cursor-paginated list endpoints respond with:

```json
{
  "data": [ /* items */ ],
  "cursor": "def456",
  "hasMore": true,
  "total": 10000
}
```

### Error response

Every error carries enough detail to debug it directly:

```json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "statusCode": 422,
    "requestId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "errors": [
      {
        "field": "email",
        "message": "Invalid email",
        "code": "invalid_string"
      }
    ],
    "suggestion": "One or more fields have incorrect types. Check that strings are quoted, numbers are unquoted, and booleans are true/false."
  },
  "timestamp": "2025-11-30T10:30:00.000Z"
}
```

**Error fields:**

* `code` — stable, machine-readable identifier you can branch on in code
* `message` — plain-language explanation of the failure
* `statusCode` — the HTTP status
* `requestId` — unique per request; quote it when you write to support
* `errors` — per-field validation breakdown, present when relevant
* `suggestion` — a hint on how to resolve the problem

The [Error Codes documentation](/api-reference/errors) covers every code with worked examples.

## Pagination

**Cursor-based** pagination is the norm for list endpoints:

```bash
GET /contacts?limit=100&cursor=abc123
```

**Parameters:**

* `limit` — page size (default: 20, max: 100)
* `cursor` — the `cursor` value returned by the previous page

**Response:**

```json
{
  "data": [ /* items */ ],
  "cursor": "def456",
  "hasMore": true,
  "total": 10000
}
```

Feed each response's `cursor` into the next request's `cursor` query parameter, and stop once `hasMore` comes back `false`. Only the first page (the one fetched without a `cursor`) computes `total`; later pages report `total: 0` so listing stays fast.

A handful of endpoints (e.g. `GET /segments/:id/contacts`) paginate **by page number** instead, taking `page` and `pageSize` and returning `total`, `page`, and `pageSize` in the response.

## Rate limits

To keep the service healthy for everyone, Bitelio applies sensible limits:

* **Email sending** — throttled per project to protect deliverability.
* **API requests** — 1000 requests/minute per project.
* **Bulk operations** — automatically queued for asynchronous processing.

Crossing a limit gets you a `429 Too Many Requests` response.

## Error codes

Errors combine standard HTTP status codes with machine-readable code identifiers:

**400 Bad Request** — Parameters are invalid or the request body is malformed

**401 Unauthorized** — API key absent or not valid

**403 Forbidden** — You lack access to the resource, or the project is disabled

**404 Not Found** — No such resource

**422 Unprocessable Entity** — Validation rejected the request (the `errors` array explains why)

**429 Too Many Requests** — You've hit a rate limit

**500 Internal Server Error** — Something failed on our side (send support the request ID)

The full catalogue, with troubleshooting steps, lives in the [Error Codes documentation](/api-reference/errors).

## API endpoints

Every endpoint the Bitelio API exposes, grouped by resource.

### Public API

The `/v1/*` routes exist for your application code and take flat, denormalised payloads. Of all endpoints, only `POST /v1/track` accepts a public (`pk_*`) key.

| Method | Path         | Description                                                                                                                  | Key    |
| ------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------- | ------ |
| POST   | `/v1/send`   | Send a transactional email — one or many recipients, template or inline content, with attachments, headers, and custom data. | `sk_*` |
| POST   | `/v1/track`  | Record an event on a contact, creating or upserting the contact as needed. Fires workflows.                                  | `pk_*` |
| POST   | `/v1/verify` | Check an email address: format, MX records, disposable domains, likely typos.                                                | `sk_*` |

### Contacts

| Method              | Path                             | Description                                                                                      |
| ------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------ |
| GET                 | `/contacts`                      | List contacts, with `search` (email substring), `limit`, and `cursor` supported.                 |
| POST                | `/contacts`                      | Create or upsert a contact keyed by email. Response includes `_meta.isNew` and `_meta.isUpdate`. |
| GET                 | `/contacts/:id`                  | Fetch one contact.                                                                               |
| PATCH               | `/contacts/:id`                  | Change a contact's email, subscription state, or `data` fields.                                  |
| DELETE              | `/contacts/:id`                  | Remove a contact.                                                                                |
| POST                | `/contacts/lookup`               | Check which of up to 500 emails already exist, in one call.                                      |
| **Custom fields**   |                                  |                                                                                                  |
| GET                 | `/contacts/fields`               | Enumerate standard and custom fields, with inferred types and coverage percentages.              |
| GET                 | `/contacts/fields/:field/values` | Distinct values of a custom field — powers segment and workflow filter UIs.                      |
| GET                 | `/contacts/fields/:field/usage`  | Everywhere a custom field appears (segments, campaigns, workflows).                              |
| DELETE              | `/contacts/fields/:field`        | Strip a custom field from every contact in the project.                                          |
| **CSV import**      |                                  |                                                                                                  |
| POST                | `/contacts/import`               | Upload a CSV (multipart, ≤ 5 MB). Runs as a queued job — returns a `jobId`.                      |
| GET                 | `/contacts/import/:jobId`        | Check on a CSV import job.                                                                       |
| **Bulk operations** |                                  |                                                                                                  |
| POST                | `/contacts/bulk-subscribe`       | Subscribe as many as 1,000 contacts by ID. Queued — returns a `jobId`.                           |
| POST                | `/contacts/bulk-unsubscribe`     | Unsubscribe as many as 1,000 contacts by ID. Queued.                                             |
| POST                | `/contacts/bulk-delete`          | Delete as many as 1,000 contacts by ID. Queued.                                                  |
| GET                 | `/contacts/bulk/:jobId`          | Check on a bulk job.                                                                             |

### Templates

| Method | Path                       | Description                                                             |
| ------ | -------------------------- | ----------------------------------------------------------------------- |
| GET    | `/templates`               | List every template.                                                    |
| POST   | `/templates`               | Create a template. Its `from` address must belong to a verified domain. |
| GET    | `/templates/:id`           | Fetch one template.                                                     |
| PATCH  | `/templates/:id`           | Edit a template.                                                        |
| DELETE | `/templates/:id`           | Remove a template.                                                      |
| POST   | `/templates/:id/duplicate` | Copy a template — the new template's ID comes back.                     |
| GET    | `/templates/:id/usage`     | Which campaigns and workflow steps rely on this template.               |

### Campaigns

| Method | Path                       | Description                                                                        |
| ------ | -------------------------- | ---------------------------------------------------------------------------------- |
| GET    | `/campaigns`               | List every campaign.                                                               |
| POST   | `/campaigns`               | Create a campaign in `DRAFT`. Its `from` address must belong to a verified domain. |
| GET    | `/campaigns/:id`           | Fetch one campaign.                                                                |
| PUT    | `/campaigns/:id`           | Replace a campaign's contents.                                                     |
| DELETE | `/campaigns/:id`           | Remove a campaign — 409 if executions are still active.                            |
| POST   | `/campaigns/:id/duplicate` | Copy a campaign — you get the new one back in `DRAFT`.                             |
| POST   | `/campaigns/:id/send`      | Send the campaign now, or later via `scheduledFor`.                                |
| POST   | `/campaigns/:id/cancel`    | Stop a campaign that is `SCHEDULED` or `SENDING`.                                  |
| POST   | `/campaigns/:id/test`      | Deliver a test to one address (`{ email: "you@example.com" }`).                    |
| GET    | `/campaigns/:id/stats`     | Current send / open / click / bounce counts.                                       |

### Segments

| Method | Path                     | Description                                                                                         |
| ------ | ------------------------ | --------------------------------------------------------------------------------------------------- |
| GET    | `/segments`              | List every segment (unpaginated — the list stays small).                                            |
| POST   | `/segments`              | Create a segment. `type: "DYNAMIC"` requires `condition`; `type: "STATIC"` rejects it.              |
| GET    | `/segments/:id`          | Fetch one segment, cached `memberCount` included.                                                   |
| PATCH  | `/segments/:id`          | Edit name, description, condition (dynamic only), or `trackMembership`.                             |
| DELETE | `/segments/:id`          | Remove a segment — 409 if an active campaign depends on it.                                         |
| GET    | `/segments/:id/contacts` | Page-based member listing via `page` and `pageSize` (max 100). Evaluated live for dynamic segments. |
| POST   | `/segments/:id/members`  | Add emails to a **static** segment. Body: `{ emails, createMissing?, subscribed? }`.                |
| DELETE | `/segments/:id/members`  | Remove emails from a **static** segment. Body: `{ emails }`.                                        |
| POST   | `/segments/:id/compute`  | Recompute a tracked dynamic segment's membership, emitting entry/exit events.                       |
| POST   | `/segments/:id/refresh`  | Lightweight count refresh — no events emitted, no membership written.                               |

### Workflows

A workflow is stored as a **workflow** record plus a graph of **steps**, the **transitions** connecting them, and one **execution** per contact passing through. The endpoints follow that same structure.

| Method          | Path                                       | Description                                                                                              |
| --------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| GET             | `/workflows`                               | List every workflow.                                                                                     |
| GET             | `/workflows/fields`                        | Contact and event fields usable inside `CONDITION` step filters.                                         |
| POST            | `/workflows`                               | Create a workflow. It always begins with `triggerType: EVENT` and `enabled: false`.                      |
| GET             | `/workflows/:id`                           | Fetch a workflow together with its steps and transitions.                                                |
| PATCH           | `/workflows/:id`                           | Edit metadata, trigger type / config, `enabled`, or `allowReentry`.                                      |
| DELETE          | `/workflows/:id`                           | Remove a workflow — only once no executions remain active (cancel or let them finish).                   |
| **Steps**       |                                            |                                                                                                          |
| POST            | `/workflows/:id/steps`                     | Add a step (`SEND_EMAIL`, `DELAY`, `WAIT_FOR_EVENT`, `CONDITION`, `WEBHOOK`, `UPDATE_CONTACT`, `EXIT`).  |
| PATCH           | `/workflows/:id/steps/:stepId`             | Edit a step's config.                                                                                    |
| DELETE          | `/workflows/:id/steps/:stepId?splice=true` | Remove a step; with `splice=true` the surrounding transitions are reconnected for you.                   |
| **Transitions** |                                            |                                                                                                          |
| POST            | `/workflows/:id/transitions`               | Connect two steps. On `CONDITION` steps, specify `branch: "yes" \| "no"`.                                |
| DELETE          | `/workflows/:id/transitions/:transitionId` | Remove a transition.                                                                                     |
| **Executions**  |                                            |                                                                                                          |
| POST            | `/workflows/:id/executions`                | Kick off an execution for a contact by hand. Accepts optional `context` JSON as per-execution variables. |
| GET             | `/workflows/:id/executions`                | List executions; filter with `status`.                                                                   |
| GET             | `/workflows/:id/executions/:executionId`   | Fetch one execution.                                                                                     |
| DELETE          | `/workflows/:id/executions/:executionId`   | Cancel an execution that is running or waiting.                                                          |
| POST            | `/workflows/:id/executions/cancel-all`     | Cancel all active executions in one go.                                                                  |

### Events

| Method | Path                         | Description                                                                                                                              |
| ------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| POST   | `/events/track`              | Separate dashboard endpoint using `name`, `contactId` and `emailId`; not the `/v1/track` payload or its idempotency/rate-limit contract. |
| GET    | `/events`                    | Recent tracked events across the project.                                                                                                |
| GET    | `/events/stats`              | Event statistics, aggregated.                                                                                                            |
| GET    | `/events/contact/:contactId` | The full event history of one contact.                                                                                                   |
| GET    | `/events/names`              | Every distinct event name the project has tracked.                                                                                       |
| GET    | `/events/:eventName/usage`   | Everywhere an event name is used (segment filters, workflow triggers, conditions).                                                       |
| DELETE | `/events/:eventName`         | Purge all events carrying that name from the project.                                                                                    |

### Domains

| Method | Path                          | Description                                                                              |
| ------ | ----------------------------- | ---------------------------------------------------------------------------------------- |
| GET    | `/domains/project/:projectId` | Domains attached to a project, verified and pending alike.                               |
| POST   | `/domains`                    | Register a domain for verification and receive the DNS records to publish.               |
| GET    | `/domains/:id/verify`         | Run a verification check immediately (the background check runs every 5 minutes anyway). |
| DELETE | `/domains/:id`                | Detach a domain.                                                                         |

### Activity & analytics

| Method | Path                        | Description                                                                                      |
| ------ | --------------------------- | ------------------------------------------------------------------------------------------------ |
| GET    | `/activity`                 | Activity feed spanning all resources (sends, opens, clicks, bounces, complaints, inbound, etc.). |
| GET    | `/activity/stats`           | Aggregate counts backing the dashboard charts.                                                   |
| GET    | `/activity/recent-count`    | Count of recent events, used by the dashboard's "live" indicator.                                |
| GET    | `/activity/types`           | The activity types present in the project.                                                       |
| GET    | `/activity/upcoming`        | Scheduled sends and executions coming up.                                                        |
| GET    | `/analytics/timeseries`     | Send / open / click counts over time.                                                            |
| GET    | `/analytics/top-campaigns`  | Best-performing campaigns, ranked by metric.                                                     |
| GET    | `/analytics/campaign-stats` | Per-campaign breakdown.                                                                          |
| GET    | `/analytics/top-events`     | Custom event names ranked by frequency.                                                          |

### Uploads

| Method | Path             | Description                                                                          |
| ------ | ---------------- | ------------------------------------------------------------------------------------ |
| POST   | `/uploads/image` | Upload an image (multipart) to embed in template bodies. Responds with a public URL. |

### Authentication & user management

These routes back the dashboard, which signs in with JWT cookies. They rarely matter for server-to-server work, but the list is included so nothing is undocumented.

| Method | Path                                          | Description                                                     |
| ------ | --------------------------------------------- | --------------------------------------------------------------- |
| POST   | `/auth/login`                                 | Log in with email + password; sets a JWT cookie.                |
| POST   | `/auth/signup`                                | Register a user (respects `DISABLE_SIGNUPS`).                   |
| GET    | `/auth/logout`                                | Drop the auth cookie.                                           |
| GET    | `/auth/oauth-config`                          | The OAuth providers currently configured.                       |
| POST   | `/auth/verify-email`                          | Confirm an email using the token from the emailed link.         |
| POST   | `/auth/request-verification`                  | Send the verification email again.                              |
| POST   | `/auth/request-password-reset`                | Email a password reset link.                                    |
| POST   | `/auth/reset-password`                        | Set a new password using a token.                               |
| GET    | `/users/@me`                                  | The currently signed-in user.                                   |
| GET    | `/users/@me/projects`                         | Projects belonging to the user.                                 |
| POST   | `/users/@me/projects`                         | Create a project.                                               |
| PATCH  | `/users/@me/projects/:id`                     | Edit project settings.                                          |
| POST   | `/users/@me/projects/:id/checkout`            | Start a Stripe Checkout session.                                |
| POST   | `/users/@me/projects/:id/billing-portal`      | Open the Stripe billing portal.                                 |
| GET    | `/users/@me/projects/:id/billing-limits`      | Read the per-category billing caps.                             |
| PUT    | `/users/@me/projects/:id/billing-limits`      | Change the per-category billing caps.                           |
| GET    | `/users/@me/projects/:id/billing-consumption` | Usage in the current billing period.                            |
| GET    | `/users/@me/projects/:id/billing-invoices`    | The project's Stripe invoices.                                  |
| GET    | `/users/@me/projects/:id/security`            | Security overview — bounce/complaint rates, recent suspensions. |
| POST   | `/users/@me/projects/:id/reset`               | Erase all project data (cannot be undone).                      |
| DELETE | `/users/@me/projects/:id`                     | Delete the whole project.                                       |
| GET    | `/projects/:id/setup-state`                   | Onboarding progress state.                                      |
| GET    | `/projects/:id/security`                      | Security state of one project.                                  |
| GET    | `/projects/:id/members`                       | The project's team members.                                     |
| POST   | `/projects/:id/members`                       | Invite someone to the team.                                     |
| PATCH  | `/projects/:id/members/:userId`               | Update a member's role.                                         |
| DELETE | `/projects/:id/members/:userId`               | Remove someone from the team.                                   |

### API keys

Scoped secret keys, per project. See the [API Keys guide](/guides/api-keys) for the permission catalog and creation rules. All three require a signed-in dashboard session with the `apikeys:manage` permission — none can be called with an API key.

| Method | Path                  | Description                                                                 |
| ------ | --------------------- | --------------------------------------------------------------------------- |
| POST   | `/apikeys`            | Create a scoped secret key. Returns its plaintext once, never again.        |
| GET    | `/apikeys`            | List the project's keys (name, prefix, scopes, timestamps — never the key). |
| POST   | `/apikeys/:id/revoke` | Revoke one key immediately; every other key keeps working.                  |

### Configuration

| Method | Path      | Description                                                                                                        |
| ------ | --------- | ------------------------------------------------------------------------------------------------------------------ |
| GET    | `/config` | Unauthenticated feature-flag endpoint — reports which integrations are on (OAuth providers, billing, S3, SMTP, …). |

### Internal webhook endpoints

The email and billing infrastructure delivers its events here. Your applications never call these — they appear only so the list is complete.

| Method | Path                        |
| ------ | --------------------------- |
| POST   | `/webhooks/sns`             |
| POST   | `/webhooks/incoming/stripe` |

## Client libraries

### Node.js

```javascript
const BITELIO_SECRET_KEY = process.env.BITELIO_SECRET_KEY;

async function sendEmail(to, subject, body) {
  const response = await fetch('https://bitelio-api.heltia.com/v1/send', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${BITELIO_SECRET_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ to, subject, body })
  });

  const data = await response.json();

  if (!data.success) {
    throw new Error(`[${data.error.code}] ${data.error.message}`);
  }

  return data.data;
}
```

### Python

```python
import os
import requests

BITELIO_SECRET_KEY = os.environ['BITELIO_SECRET_KEY']

def send_email(to, subject, body):
    response = requests.post(
        'https://bitelio-api.heltia.com/v1/send',
        headers={
            'Authorization': f'Bearer {BITELIO_SECRET_KEY}',
            'Content-Type': 'application/json'
        },
        json={'to': to, 'subject': subject, 'body': body}
    )

    data = response.json()

    if not data.get('success'):
        error = data['error']
        raise Exception(f"[{error['code']}] {error['message']}")

    return data['data']
```

### cURL

```bash
curl -X POST https://bitelio-api.heltia.com/v1/send \
  -H "Authorization: Bearer $BITELIO_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to": "user@example.com", "subject": "Hello", "body": "Message"}'
```

## What's next

* [Send a transactional email](/api-reference/public-api/sendEmail)
* [Track an event](/api-reference/public-api/trackEvent)
* [Error codes reference](/api-reference/errors)
