BitelioBitelio

API Reference

Everything the Bitelio API exposes — auth, request shapes, pagination, rate limits, and the full endpoint catalogue

Base URL

https://api.bitelio.com

Every 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 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 covers every code with worked examples.

Pagination

Cursor-based pagination is the norm for list endpoints:

GET /contacts?limit=100&cursor=abc123

Parameters:

  • limit — page size (default: 20, max: 100)
  • cursor — the cursor value 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.

MethodPathDescriptionKey
POST/v1/sendSend a transactional email — one or many recipients, template or inline content, with attachments, headers, and custom data.sk_*
POST/v1/trackRecord an event on a contact, creating or upserting the contact as needed. Fires workflows.pk_* or sk_*
POST/v1/verifyCheck an email address: format, MX records, disposable domains, likely typos.sk_*

Contacts

MethodPathDescription
GET/contactsList contacts, with search (email substring), limit, and cursor supported.
POST/contactsCreate or upsert a contact keyed by email. Response includes _meta.isNew and _meta.isUpdate.
GET/contacts/:idFetch one contact.
PATCH/contacts/:idChange a contact's email, subscription state, or data fields.
DELETE/contacts/:idRemove a contact.
POST/contacts/lookupCheck which of up to 500 emails already exist, in one call.
Custom fields
GET/contacts/fieldsEnumerate standard and custom fields, with inferred types and coverage percentages.
GET/contacts/fields/:field/valuesDistinct values of a custom field — powers segment and workflow filter UIs.
GET/contacts/fields/:field/usageEverywhere a custom field appears (segments, campaigns, workflows).
DELETE/contacts/fields/:fieldStrip a custom field from every contact in the project.
CSV import
POST/contacts/importUpload a CSV (multipart, ≤ 5 MB). Runs as a queued job — returns a jobId.
GET/contacts/import/:jobIdCheck on a CSV import job.
Bulk operations
POST/contacts/bulk-subscribeSubscribe as many as 1,000 contacts by ID. Queued — returns a jobId.
POST/contacts/bulk-unsubscribeUnsubscribe as many as 1,000 contacts by ID. Queued.
POST/contacts/bulk-deleteDelete as many as 1,000 contacts by ID. Queued.
GET/contacts/bulk/:jobIdCheck on a bulk job.

Templates

MethodPathDescription
GET/templatesList every template.
POST/templatesCreate a template. Its from address must belong to a verified domain.
GET/templates/:idFetch one template.
PATCH/templates/:idEdit a template.
DELETE/templates/:idRemove a template.
POST/templates/:id/duplicateCopy a template — the new template's ID comes back.
GET/templates/:id/usageWhich campaigns and workflow steps rely on this template.

Campaigns

MethodPathDescription
GET/campaignsList every campaign.
POST/campaignsCreate a campaign in DRAFT. Its from address must belong to a verified domain.
GET/campaigns/:idFetch one campaign.
PUT/campaigns/:idReplace a campaign's contents.
DELETE/campaigns/:idRemove a campaign — 409 if executions are still active.
POST/campaigns/:id/duplicateCopy a campaign — you get the new one back in DRAFT.
POST/campaigns/:id/sendSend the campaign now, or later via scheduledFor.
POST/campaigns/:id/cancelStop a campaign that is SCHEDULED or SENDING.
POST/campaigns/:id/testDeliver a test to one address ({ email: "you@example.com" }).
GET/campaigns/:id/statsCurrent send / open / click / bounce counts.

Segments

MethodPathDescription
GET/segmentsList every segment (unpaginated — the list stays small).
POST/segmentsCreate a segment. type: "DYNAMIC" requires condition; type: "STATIC" rejects it.
GET/segments/:idFetch one segment, cached memberCount included.
PATCH/segments/:idEdit name, description, condition (dynamic only), or trackMembership.
DELETE/segments/:idRemove a segment — 409 if an active campaign depends on it.
GET/segments/:id/contactsPage-based member listing via page and pageSize (max 100). Evaluated live for dynamic segments.
POST/segments/:id/membersAdd emails to a static segment. Body: { emails, createMissing?, subscribed? }.
DELETE/segments/:id/membersRemove emails from a static segment. Body: { emails }.
POST/segments/:id/computeRecompute a tracked dynamic segment's membership, emitting entry/exit events.
POST/segments/:id/refreshLightweight 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.

MethodPathDescription
GET/workflowsList every workflow.
GET/workflows/fieldsContact and event fields usable inside CONDITION step filters.
POST/workflowsCreate a workflow. It always begins with triggerType: EVENT and enabled: false.
GET/workflows/:idFetch a workflow together with its steps and transitions.
PATCH/workflows/:idEdit metadata, trigger type / config, enabled, or allowReentry.
DELETE/workflows/:idRemove a workflow — only once no executions remain active (cancel or let them finish).
Steps
POST/workflows/:id/stepsAdd a step (SEND_EMAIL, DELAY, WAIT_FOR_EVENT, CONDITION, WEBHOOK, UPDATE_CONTACT, EXIT).
PATCH/workflows/:id/steps/:stepIdEdit a step's config.
DELETE/workflows/:id/steps/:stepId?splice=trueRemove a step; with splice=true the surrounding transitions are reconnected for you.
Transitions
POST/workflows/:id/transitionsConnect two steps. On CONDITION steps, specify branch: "yes" | "no".
DELETE/workflows/:id/transitions/:transitionIdRemove a transition.
Executions
POST/workflows/:id/executionsKick off an execution for a contact by hand. Accepts optional context JSON as per-execution variables.
GET/workflows/:id/executionsList executions; filter with status.
GET/workflows/:id/executions/:executionIdFetch one execution.
DELETE/workflows/:id/executions/:executionIdCancel an execution that is running or waiting.
POST/workflows/:id/executions/cancel-allCancel all active executions in one go.

Events

MethodPathDescription
POST/events/trackDashboard-facing alias of /v1/track. Application code should call /v1/track instead.
GET/eventsRecent tracked events across the project.
GET/events/statsEvent statistics, aggregated.
GET/events/contact/:contactIdThe full event history of one contact.
GET/events/namesEvery distinct event name the project has tracked.
GET/events/:eventName/usageEverywhere an event name is used (segment filters, workflow triggers, conditions).
DELETE/events/:eventNamePurge all events carrying that name from the project.

Domains

MethodPathDescription
GET/domains/project/:projectIdDomains attached to a project, verified and pending alike.
POST/domainsRegister a domain for verification and receive the DNS records to publish.
GET/domains/:id/verifyRun a verification check immediately (the background check runs every 5 minutes anyway).
DELETE/domains/:idDetach a domain.

Activity & analytics

MethodPathDescription
GET/activityActivity feed spanning all resources (sends, opens, clicks, bounces, complaints, inbound, etc.).
GET/activity/statsAggregate counts backing the dashboard charts.
GET/activity/recent-countCount of recent events, used by the dashboard's "live" indicator.
GET/activity/typesThe activity types present in the project.
GET/activity/upcomingScheduled sends and executions coming up.
GET/analytics/timeseriesSend / open / click counts over time.
GET/analytics/top-campaignsBest-performing campaigns, ranked by metric.
GET/analytics/campaign-statsPer-campaign breakdown.
GET/analytics/top-eventsCustom event names ranked by frequency.

Uploads

MethodPathDescription
POST/uploads/imageUpload 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.

MethodPathDescription
POST/auth/loginLog in with email + password; sets a JWT cookie.
POST/auth/signupRegister a user (respects DISABLE_SIGNUPS).
GET/auth/logoutDrop the auth cookie.
GET/auth/oauth-configThe OAuth providers currently configured.
POST/auth/verify-emailConfirm an email using the token from the emailed link.
POST/auth/request-verificationSend the verification email again.
POST/auth/request-password-resetEmail a password reset link.
POST/auth/reset-passwordSet a new password using a token.
GET/users/@meThe currently signed-in user.
GET/users/@me/projectsProjects belonging to the user.
POST/users/@me/projectsCreate a project.
PATCH/users/@me/projects/:idEdit project settings.
POST/users/@me/projects/:id/checkoutStart a Stripe Checkout session.
POST/users/@me/projects/:id/billing-portalOpen the Stripe billing portal.
GET/users/@me/projects/:id/billing-limitsRead the per-category billing caps.
PUT/users/@me/projects/:id/billing-limitsChange the per-category billing caps.
GET/users/@me/projects/:id/billing-consumptionUsage in the current billing period.
GET/users/@me/projects/:id/billing-invoicesThe project's Stripe invoices.
GET/users/@me/projects/:id/securitySecurity overview — bounce/complaint rates, recent suspensions.
POST/users/@me/projects/:id/resetErase all project data (cannot be undone).
DELETE/users/@me/projects/:idDelete the whole project.
GET/projects/:id/setup-stateOnboarding progress state.
GET/projects/:id/securitySecurity state of one project.
GET/projects/:id/membersThe project's team members.
POST/projects/:id/membersInvite someone to the team.
PATCH/projects/:id/members/:userIdUpdate a member's role.
DELETE/projects/:id/members/:userIdRemove 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.

MethodPathDescription
POST/apikeysCreate a scoped secret key. Returns its plaintext once, never again.
GET/apikeysList the project's keys (name, prefix, scopes, timestamps — never the key).
POST/apikeys/:id/revokeRevoke one key immediately; every other key keeps working.

Configuration

MethodPathDescription
GET/configUnauthenticated 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.

MethodPath
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"}'

What's next