API Reference
Everything the Bitelio API exposes — auth, request shapes, pagination, rate limits, and the full endpoint catalogue
Base URL
https://api.bitelio.comEvery request goes to this base URL.
Authentication
Pass your API key as a bearer token in the Authorization header:
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
curl -X POST https://api.bitelio.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
curl -X POST https://api.bitelio.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
curl -X POST https://api.bitelio.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:
{
"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:
{
"id": "cnt_abc123",
"email": "user@example.com",
"createdAt": "2025-11-30T10:30:00.000Z"
}Cursor-paginated list endpoints respond with:
{
"data": [ /* items */ ],
"cursor": "def456",
"hasMore": true,
"total": 10000
}Error response
Every error carries enough detail to debug it directly:
{
"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 codemessage— plain-language explanation of the failurestatusCode— the HTTP statusrequestId— unique per request; quote it when you write to supporterrors— per-field validation breakdown, present when relevantsuggestion— a hint on how to resolve the problem
The Error Codes documentation covers every code with worked examples.
Pagination
Cursor-based pagination is the norm for list endpoints:
GET /contacts?limit=100&cursor=abc123Parameters:
limit— page size (default: 20, max: 100)cursor— thecursorvalue returned by the previous page
Response:
{
"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 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_* or sk_* |
| 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 | Dashboard-facing alias of /v1/track. Application code should call /v1/track instead. |
| 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 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
const BITELIO_SECRET_KEY = process.env.BITELIO_SECRET_KEY;
async function sendEmail(to, subject, body) {
const response = await fetch('https://api.bitelio.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
import os
import requests
BITELIO_SECRET_KEY = os.environ['BITELIO_SECRET_KEY']
def send_email(to, subject, body):
response = requests.post(
'https://api.bitelio.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
curl -X POST https://api.bitelio.com/v1/send \
-H "Authorization: Bearer $BITELIO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "user@example.com", "subject": "Hello", "body": "Message"}'