API Documentation

API, Webhooks and Integrations

Reachkit provides a REST API that allows you to integrate our platform with your existing systems and automate your cold email outreach workflows.

Note: API access and integrations are available exclusively on the Scale plan.

Getting Started

The Reachkit API gives you programmatic access to manage inboxes, campaigns, leads, and more. This documentation covers all available endpoints, with examples of request and response formats.

Who should use the API?

Our API is designed for:

  • Developers integrating Reachkit with other business systems
  • Power users who want to automate repetitive tasks
  • Teams building custom reporting or dashboards

Common use cases

  • Synchronizing leads between your CRM and Reachkit
  • Automatically creating and managing campaigns
  • Building custom analytics dashboards
  • Integrating Reachkit with your internal tools

Authentication

All API requests require authentication using an API key. You can provide your API key in one of three ways:

  1. Authorization Header (Bearer Token)

    Authorization: Bearer your_api_key_here
  2. X-API-Key Header

    X-API-Key: your_api_key_here
  3. Query Parameter

    GET /api/v1/campaigns?api_key=your_api_key_here

To obtain an API key:

  1. Navigate to SettingsIntegrations
  2. Click Add API Key
  3. Enter a name for the key
  4. Store your key securely - it will only be shown once

Response Format

All responses are returned in JSON format with a standard structure:

  • Success responses include a data field containing the requested information
  • Error responses include an error field with a message describing the error and sometimes a details field with more specific information

API Endpoints

The following sections provide detailed information about all available API endpoints, organized by resource type.

Blocklist Endpoints

Check email

GET /api/v1/blocklist/check

Checks if an email is in the blocklist.

Query Parameters:

  • email: string (required) - Email address to check

Response:

{
  "data": {
    "blocked": boolean
  }
}

Create

POST /api/v1/blocklist

Creates new blocklist entries.

Request:

{
  "email": string  # Email address to add to blocklist
}

Response:

{
  "data": {
    "id": string,
    "email": string,
    "created_at": string
  }
}

Delete

DELETE /api/v1/blocklist/:id

Deletes a blocklist entry.

Response: 204 No Content


Index

GET /api/v1/blocklist

Lists all blocklist entries for the authenticated user.

Query Parameters:

  • page_size: integer (optional, default: 30, max: 100) - Number of entries per page
  • page: integer (optional, default: 1) - Page number
  • search: string (optional) - Search term for entries
  • sort_by: string (optional) - Field to sort by
  • sort_order: string (optional, “asc” or “desc”) - Sort direction

Response:

{
  "data": [
    {
      "id": string,
      "email": string,
      "created_at": string
    }
  ],
  "metadata": {
    "page": integer,
    "page_size": integer,
    "total_count": integer,
    "total_pages": integer
  }
}

Campaign Endpoints

Analytics

GET /api/v1/campaigns/:campaign_id/analytics

Returns the full analytics view for a campaign — the headline numbers inline, plus a daily array (last 30 days) and a sequences array (per-sequence and per-variant performance).

The same headline fields are also embedded as data.stats.* on GET /campaigns/:id and GET /campaigns, so for “show me this campaign” use cases you can read them inline without an extra request.

Response:

{
  "data": {
    "progress": integer,           # Campaign progress percentage (0-100)
    "sequence_steps": integer,     # Number of sequence steps
    "leads_count": integer,        # Total number of leads
    "sent": integer,               # Total emails sent
    "contacted": integer,          # Distinct leads who received at least one email
    "replies": integer,            # Total replies received (distinct leads)
    "reply_rate": float,           # Reply rate percentage (replies / contacted)
    "positive_replies": integer,   # Number of positive conversations
    "positive_reply_rate": float,  # Positive reply rate percentage
    "bounced": integer,            # Number of bounced emails
    "bounce_rate": float,          # Bounce rate percentage
    "daily": [
      # 30 days of data (today and previous 29 days)
      {
        "date": string,            # Format: YYYY-MM-DD
        "sent": integer,           # Emails sent that day
        "replies": integer         # Replies received that day
      }
    ],
    "sequences": [
      {
        "id": string,
        "step": integer,
        "variants": [
          {
            "id": string,
            "enabled": boolean,
            "sent": integer,
            "replies": integer,
            "reply_rate": float,
            "positive_replies": integer,
            "positive_reply_rate": float
          }
        ]
      }
    ]
  }
}

Attach inboxes

POST /api/v1/campaigns/:campaign_id/inboxes/attach

Attaches a set of inboxes to a campaign. Idempotent: inboxes already attached are silently kept, and duplicates in the request are deduped. All-or-nothing on authorization: if any id is unknown or belongs to a different workspace, the request returns 404 and no changes are applied.

Request:

{
  "inbox_ids": [string] # one or more inbox UUIDs from this workspace
}

Response (the full set of inboxes attached to this campaign after the operation; same projection used inline on GET /campaigns/:id):

{
  "data": [
    {
      "id": string,
      "email": string,
      "provider": string,           # one of: google, microsoft, other
      "is_active": boolean,
      "connection_status": string   # one of: pending, validating, connected, error
    }
  ]
}

Create

POST /api/v1/campaigns

Creates a new campaign in the authenticated workspace. Only name is required — sensible defaults are applied for everything else (an empty first sequence and variant are created so the campaign is editable immediately). Inboxes, leads, schedule, and options are added via their own endpoints.

Request:

{
  "name": string  # required, max 150 chars
}

Response (campaign is created in draft status):

{
  "data": {
    "id": string,
    "name": string,
    "status": string,             # one of: draft, active, paused
    "progress": integer,          # 0-100, cached campaign progress

    "options": {
      "daily_limit": integer,
      "stop_on_reply": boolean,
      "stop_on_company_reply": boolean,
      "text_only": boolean,
      "esp_matching": boolean,
      "unsubscribe_option": boolean
    },

    "schedule": {
      "timezone": string,                              # IANA, e.g. "America/Chicago"
      "default_window": {"start": "HH:MM", "end": "HH:MM"},
      "days": {
        "monday":    {"enabled": boolean, "start": string|null, "end": string|null},
        "tuesday":   {...},
        ...
      }
    },

    "stats": {
      "progress": integer, "sequence_steps": integer, "leads_count": integer,
      "sent": integer, "contacted": integer,
      "replies": integer, "reply_rate": float,
      "positive_replies": integer, "positive_reply_rate": float,
      "bounced": integer, "bounce_rate": float,
      "daily": [
        {"date": "YYYY-MM-DD", "sent": integer, "replies": integer}
      ],
      "sequences": [
        {"id": string, "step": integer, "variants": [
          {"id": string, "enabled": boolean, "sent": integer, "replies": integer,
           "reply_rate": float, "positive_replies": integer, "positive_reply_rate": float}
        ]}
      ]
    },

    "inboxes": [
      {
        "id": string,
        "email": string,
        "provider": string,           # one of: google, microsoft, other
        "is_active": boolean,
        "connection_status": string   # one of: pending, validating, connected, error
      }
    ],
    "created_at": string,
    "updated_at": string
  }
}

Delete

DELETE /api/v1/campaigns/:id

Permanently deletes a campaign and the resources owned by it: its schedule, sequences, variants, sent emails, and leads.

This is not reversible. Returns 204 on success.


Detach inboxes

POST /api/v1/campaigns/:campaign_id/inboxes/detach

Detaches a set of inboxes from a campaign. Idempotent: inboxes that weren’t attached are silently skipped. All-or-nothing on authorization: if any id is unknown or belongs to a different workspace, returns 404 and nothing is detached.

Request:

{
  "inbox_ids": [string] # one or more inbox UUIDs from this workspace
}

Response (the full set of inboxes attached to this campaign after the operation; same projection used inline on GET /campaigns/:id):

{
  "data": [
    {
      "id": string,
      "email": string,
      "provider": string,           # one of: google, microsoft, other
      "is_active": boolean,
      "connection_status": string   # one of: pending, validating, connected, error
    }
  ]
}

Index

GET /api/v1/campaigns

Lists campaigns for the authenticated workspace.

Each item is the full campaign projection — identity, status, options, schedule, summary stats, and associated inboxes. List items omit the daily array and per-sequence breakdowns from stats; for those, fetch a single campaign via GET /campaigns/:id.

Query Parameters:

  • page_size: integer (optional, default: 30, max: 100)
  • page: integer (optional, default: 1)
  • search: string (optional)
  • sort_by: string (optional)
  • sort_order: string (optional, “asc” or “desc”)

Response:

{
  "data": [
    {
      "id": string,
      "name": string,
      "status": string,             # one of: draft, active, paused
      "progress": integer,          # 0-100, cached campaign progress

      "options": {
        "daily_limit": integer,
        "stop_on_reply": boolean,
        "stop_on_company_reply": boolean,
        "text_only": boolean,
        "esp_matching": boolean,
        "unsubscribe_option": boolean
      },

      "schedule": {
        "timezone": string,                              # IANA, e.g. "America/Chicago"
        "default_window": {"start": "HH:MM", "end": "HH:MM"},
        "days": {
          "monday":    {"enabled": boolean, "start": string|null, "end": string|null},
          "tuesday":   {...},
          ...
        }
      },

      "stats": {
        "progress": integer, "sequence_steps": integer, "leads_count": integer,
        "sent": integer, "contacted": integer,
        "replies": integer, "reply_rate": float,
        "positive_replies": integer, "positive_reply_rate": float,
        "bounced": integer, "bounce_rate": float
      },

      "inboxes": [
        {
          "id": string,
          "email": string,
          "provider": string,           # one of: google, microsoft, other
          "is_active": boolean,
          "connection_status": string   # one of: pending, validating, connected, error
        }
      ],
      "created_at": string,
      "updated_at": string
    }
  ],
  "metadata": {
    "page": integer,
    "page_size": integer,
    "total_count": integer,
    "total_pages": integer
  }
}

Show

GET /api/v1/campaigns/:id

Shows details for a specific campaign.

Response:

{
  "data": {
    "id": string,
    "name": string,
    "status": string,             # one of: draft, active, paused
    "progress": integer,          # 0-100, cached campaign progress

    "options": {
      "daily_limit": integer,
      "stop_on_reply": boolean,
      "stop_on_company_reply": boolean,
      "text_only": boolean,
      "esp_matching": boolean,
      "unsubscribe_option": boolean
    },

    "schedule": {
      "timezone": string,                              # IANA, e.g. "America/Chicago"
      "default_window": {"start": "HH:MM", "end": "HH:MM"},
      "days": {
        "monday":    {"enabled": boolean, "start": string|null, "end": string|null},
        "tuesday":   {...},
        ...
      }
    },

    "stats": {
      "progress": integer, "sequence_steps": integer, "leads_count": integer,
      "sent": integer, "contacted": integer,
      "replies": integer, "reply_rate": float,
      "positive_replies": integer, "positive_reply_rate": float,
      "bounced": integer, "bounce_rate": float,
      "daily": [
        {"date": "YYYY-MM-DD", "sent": integer, "replies": integer}
      ],
      "sequences": [
        {"id": string, "step": integer, "variants": [
          {"id": string, "enabled": boolean, "sent": integer, "replies": integer,
           "reply_rate": float, "positive_replies": integer, "positive_reply_rate": float}
        ]}
      ]
    },

    "inboxes": [
      {
        "id": string,
        "email": string,
        "provider": string,           # one of: google, microsoft, other
        "is_active": boolean,
        "connection_status": string   # one of: pending, validating, connected, error
      }
    ],
    "created_at": string,
    "updated_at": string
  }
}

Update

PATCH /api/v1/campaigns/:id

Updates a campaign. Provide any combination of the fields below; omitted fields are unchanged. All updates apply together — if any field fails validation, none of the requested changes are saved. Schedule changes have their own endpoint.

  • name (string, max 150)
  • status (one of: active, paused) — only valid transitions are accepted; you cannot launch a campaign without at least one variant and one inbox
  • daily_limit (integer, ≥ 0) — total daily sending across all inboxes
  • stop_on_reply (boolean)
  • stop_on_company_reply (boolean)
  • text_only (boolean) — sends as plain text only, omitting the HTML body
  • unsubscribe_option (boolean) — adds the List-Unsubscribe header and an {{unsubscribe_url}} link to every variant body when enabled; removes both when disabled
  • esp_matching (boolean) — only send to leads whose ESP matches the sending inbox’s ESP

Response:

{
  "data": {
    "id": string,
    "name": string,
    "status": string,             # one of: draft, active, paused
    "progress": integer,          # 0-100, cached campaign progress

    "options": {
      "daily_limit": integer,
      "stop_on_reply": boolean,
      "stop_on_company_reply": boolean,
      "text_only": boolean,
      "esp_matching": boolean,
      "unsubscribe_option": boolean
    },

    "schedule": {
      "timezone": string,                              # IANA, e.g. "America/Chicago"
      "default_window": {"start": "HH:MM", "end": "HH:MM"},
      "days": {
        "monday":    {"enabled": boolean, "start": string|null, "end": string|null},
        "tuesday":   {...},
        ...
      }
    },

    "stats": {
      "progress": integer, "sequence_steps": integer, "leads_count": integer,
      "sent": integer, "contacted": integer,
      "replies": integer, "reply_rate": float,
      "positive_replies": integer, "positive_reply_rate": float,
      "bounced": integer, "bounce_rate": float
    },

    "inboxes": [
      {
        "id": string,
        "email": string,
        "provider": string,           # one of: google, microsoft, other
        "is_active": boolean,
        "connection_status": string   # one of: pending, validating, connected, error
      }
    ],
    "created_at": string,
    "updated_at": string
  }
}

Update schedule

PATCH /api/v1/campaigns/:campaign_id/schedule

Updates the campaign’s sending schedule. Provide any combination of timezone, default_window, and per-day overrides. Omitted fields are unchanged.

Per-day start/end override the default_window for that day. Setting either to null falls back to the default window. enabled: false stops sending entirely on that day regardless of window.

Time strings are 24-hour "HH:MM". Times are stored on a 30-minute grid to match the UI: minutes are rounded to the nearest 00 or 30 (ties round up), and the maximum is 23:30. The response echoes the stored (rounded) value, so a request with "09:23" returns "09:30".

Timezone is an IANA zone name from the supported list below. Any other value returns 422 with a timezone validation error.

Supported zones:

  • Pacific/Honolulu, America/Anchorage, America/Los_Angeles, America/Vancouver, America/Phoenix, America/Denver, America/Chicago, America/New_York, America/Toronto, America/Sao_Paulo
  • Europe/London, Europe/Paris, Europe/Berlin, Europe/Kiev, Europe/Moscow
  • Asia/Tehran, Asia/Dubai, Asia/Karachi, Asia/Kolkata, Asia/Bangkok, Asia/Shanghai, Asia/Hong_Kong, Asia/Singapore, Asia/Tokyo
  • Australia/Sydney, Pacific/Auckland

Request:

{
  "timezone": string,                                          # optional
  "default_window": {"start": "HH:MM", "end": "HH:MM"},  # optional
  "days": {                                                     # optional, any subset of days
    "monday":    {"enabled": boolean, "start": "HH:MM"|null, "end": "HH:MM"|null},
    "tuesday":   {...},
    "wednesday": {...},
    "thursday":  {...},
    "friday":    {...},
    "saturday":  {...},
    "sunday":    {...}
  }
}

Response:

{
  "data": {
    "timezone": string,
    "default_window": {"start": "HH:MM", "end": "HH:MM"},
    "days": {
      "monday":    {"enabled": boolean, "start": string|null, "end": string|null},
      "tuesday":   {...},
      "wednesday": {...},
      "thursday":  {...},
      "friday":    {...},
      "saturday":  {...},
      "sunday":    {...}
    }
  }
}

Conversation Endpoints

Create

POST /api/v1/unibox/conversations

Creates a new conversation and sends the first email.

Request:

{
  "inbox_id": string,             # ID of the inbox to send from
  "to": [                          # Recipients (at least one required)
    {"email": string, "name": string|null}
  ],
  "subject": string,               # Email subject line
  "content": string                # HTML content of the email
}

If inbox_id is omitted, the first active inbox in the workspace is used. Returns 422 if no active inbox exists.

Response:

{
  "data": {
    "id": string,
    "label": string|null,
    "is_positive": boolean,
    "lead_id": null,
    "messages": [...],
    "created_at": string,
    "updated_at": string
  }
}

Download attachment

GET /api/v1/unibox/conversations/:conversation_id/attachments/:id

Downloads an attachment from a conversation message.

Response: Binary file content with appropriate content-type header


Index

GET /api/v1/unibox/conversations

Lists all conversations.

Query Parameters:

  • page_size: integer (optional, default: 20, max: 100) - Number of conversations per page
  • page: integer (optional, default: 1) - Page number
  • search: string (optional) - Search term for conversations
  • folder: string (optional) - Filter by folder (“primary” or “others”)

Response:

{
  "data": [
    {
      "id": string,
      "label": string|null,   # Effective label: interested, not_interested, meeting_requested, etc.
      "is_positive": boolean,  # Whether the conversation is positively engaged
      "lead_id": string|null,  # Associated lead ID (null for manual conversations)
      "messages": [
        {
          "direction": string,  # One of: outgoing, incoming
          "inbox_id": string,   # ID of the inbox this message went through
          "subject": string,
          "text_body": string,
          "html_body": string,
          "from_email": string,
          "to_recipients": [{"email": string, "name": string}],
          "cc_recipients": [{"email": string, "name": string}],
          "bcc_recipients": [{"email": string, "name": string}],
          "email_date": string,
          "is_read": boolean,
          "attachments": [
            {
              "id": string,
              "filename": string,
              "content_type": string,
              "size": integer
            }
          ]
        }
      ],
      "created_at": string,
      "updated_at": string
    }
  ],
  "metadata": {
    "page": integer,
    "page_size": integer,
    "total_count": integer,
    "total_pages": integer
  }
}

Reply

POST /api/v1/unibox/conversations/:conversation_id/reply

Reply to a conversation.

Request:

{
  "content": string           # HTML content of the reply
}

Response:

{
  "data": {
    "direction": string,      # Will be "outgoing"
    "inbox_id": string,       # ID of the inbox the reply went through
    "subject": string,
    "text_body": string,
    "html_body": string,
    "from_email": string,
    "to_recipients": [{"email": string, "name": string}],
    "cc_recipients": [{"email": string, "name": string}],
    "bcc_recipients": [{"email": string, "name": string}],
    "email_date": string,
    "is_read": boolean,
    "attachments": []
  }
}

Show

GET /api/v1/unibox/conversations/:id

Shows details for a specific conversation.

Response:

{
  "data": {
    "id": string,
    "label": string|null,   # Effective label: interested, not_interested, meeting_requested, etc.
    "is_positive": boolean,  # Whether the conversation is positively engaged
    "lead_id": string|null,  # Associated lead ID (null for manual conversations)
    "messages": [
      {
        "direction": string,  # One of: outgoing, incoming
        "subject": string,
        "text_body": string,
        "html_body": string,
        "from_email": string,
        "to_recipients": [{"email": string, "name": string}],
        "cc_recipients": [{"email": string, "name": string}],
        "bcc_recipients": [{"email": string, "name": string}],
        "email_date": string,
        "is_read": boolean,
        "attachments": [
          {
            "id": string,
            "filename": string,
            "content_type": string,
            "size": integer
          }
        ]
      }
    ],
    "created_at": string,
    "updated_at": string
  }
}

Update

PATCH /api/v1/unibox/conversations/:id

Updates a conversation. Provide any combination of label_override and is_read. Omitted fields are unchanged.

label_override accepts one of: interested, not_interested, needs_more_info, meeting_requested, wrong_person, out_of_office, referral, question, neutral, unsubscribe, left_company. Pass null to clear the override and revert to the AI-classified label.

is_read: true marks every message in the conversation as read. is_read: false marks every message as unread, useful for surfacing a thread back into triage.

Request:

{
  "label_override": string|null,  # optional
  "is_read": boolean              # optional
}

Response:

{
  "data": {
    "id": string,
    "label": string|null,   # Effective label: interested, not_interested, meeting_requested, etc.
    "is_positive": boolean,  # Whether the conversation is positively engaged
    "lead_id": string|null,  # Associated lead ID (null for manual conversations)
    "messages": [
      {
        "direction": string,  # One of: outgoing, incoming
        "inbox_id": string,   # ID of the inbox this message went through
        "subject": string,
        "text_body": string,
        "html_body": string,
        "from_email": string,
        "to_recipients": [{"email": string, "name": string}],
        "cc_recipients": [{"email": string, "name": string}],
        "bcc_recipients": [{"email": string, "name": string}],
        "email_date": string,
        "is_read": boolean,
        "attachments": [
          {
            "id": string,
            "filename": string,
            "content_type": string,
            "size": integer
          }
        ]
      }
    ],
    "created_at": string,
    "updated_at": string
  }
}

Inbox Endpoints

Index

GET /api/v1/inboxes

Lists all inboxes for the authenticated user.

Query Parameters:

  • page_size: integer (optional, default: 30, max: 100) - Number of inboxes per page
  • page: integer (optional, default: 1) - Page number
  • search: string (optional) - Search term for inboxes
  • sort_by: string (optional) - Field to sort by
  • sort_order: string (optional, “asc” or “desc”) - Sort direction

Response:

{
  "data": [
    {
      "id": string,
      "email": string,
      "domain": string,
      "provider": string,            # one of: google, microsoft, other
      "connection_type": string,     # one of: oauth, imap_smtp
      "connection_status": string,   # one of: pending, validating, connected, error
      "first_name": string,
      "last_name": string,
      "reply_to": string,
      "signature": string,
      "is_active": boolean,
      "is_warming": boolean,
      "daily_campaign_limit": integer,
      "daily_warmup_limit": integer,
      "created_at": string,
      "updated_at": string
    }
  ],
  "metadata": {
    "page": integer,
    "page_size": integer,
    "total_count": integer,
    "total_pages": integer
  }
}

Show

GET /api/v1/inboxes/:id

Shows details for a specific inbox.

Response:

{
  "data": {
    "id": string,
    "email": string,
    "domain": string,
    "provider": string,            # one of: google, microsoft, other
    "connection_type": string,     # one of: oauth, imap_smtp
    "connection_status": string,   # one of: pending, validating, connected, error
    "first_name": string,
    "last_name": string,
    "reply_to": string,
    "signature": string,
    "is_active": boolean,
    "is_warming": boolean,
    "daily_campaign_limit": integer,
    "daily_warmup_limit": integer,
    "campaigns": [
      {
        "id": string,
        "name": string,
        "status": string,
        "created_at": string,
        "updated_at": string
      }
    ],
    "created_at": string,
    "updated_at": string
  }
}

Update

PATCH /api/v1/inboxes/:id

Updates editable settings on an inbox. Provide any combination of the fields below; omitted fields are unchanged.

To stop sending from an inbox, set is_active: false.

Request:

{
  "is_active": boolean,             # optional — pause/resume campaign sending
  "is_warming": boolean,            # optional — toggle warmup
  "first_name": string,             # optional
  "last_name": string,              # optional
  "reply_to": string,               # optional — empty string clears it
  "signature": string,              # optional — appended to the bottom of every outgoing email
  "daily_campaign_limit": integer,  # optional — between 1 and 500
  "daily_warmup_limit": integer     # optional — between 1 and 100
}

Response:

{
  "data": {
    "id": string,
    "email": string,
    "domain": string,
    "provider": string,            # one of: google, microsoft, other
    "connection_type": string,     # one of: oauth, imap_smtp
    "connection_status": string,   # one of: pending, validating, connected, error
    "first_name": string,
    "last_name": string,
    "reply_to": string,
    "signature": string,
    "is_active": boolean,
    "is_warming": boolean,
    "daily_campaign_limit": integer,
    "daily_warmup_limit": integer,
    "campaigns": [
      {
        "id": string,
        "name": string,
        "status": string,
        "created_at": string,
        "updated_at": string
      }
    ],
    "created_at": string,
    "updated_at": string
  }
}

Warmup stats

GET /api/v1/inboxes/:inbox_id/warmup/stats

Returns warmup statistics for an inbox over the last 7 days.

Response:

{
  "data": {
    "sent": integer,             # Total warmup emails sent
    "received": integer,         # Total warmup emails received
    "saved_from_spam": integer,  # Emails moved from spam folder
    "health_score": integer,     # 0-100 score based on spam rate
    "ramp_up_progress": integer, # 0-100 percentage of warmup progress
    "daily": [
      {
        "date": string,          # Format: YYYY-MM-DD
        "sent": integer,         # Emails sent that day
        "spam_count": integer    # Emails that went to spam that day
      }
    ]
  }
}

Lead Endpoints

Create

POST /api/v1/campaigns/:campaign_id/leads

Creates a new lead for a campaign.

Custom variables feed sequence templates: reference any key as {{key}} in a variant subject or body and the rendered email will interpolate the value. Variable names are normalized on match, so {{First Name}}, {{first_name}}, and {{firstName}} all resolve to the same key. Use company for the lead’s company name.

Every lead also exposes built-in variables: first_name, last_name, email, full_name, contact.

Request:

{
  "email": string,           # Required
  "first_name": string,      # Optional
  "last_name": string,       # Optional
  "variables": object,       # Optional, e.g. {"company": "Acme", "role": "VP Sales"}
  "if_exists": string        # Optional: "fail" (default), "skip", "update"
}

Response:

{
  "data": {
    "id": string,
    "email": string,
    "first_name": string,
    "last_name": string,
    "status": string,              # One of: uncontacted, contacted, replied
    "is_bounced": boolean,         # Whether the lead has bounced
    "is_unsubscribed": boolean,    # Whether the lead has unsubscribed
    "provider": string|null,       # null while ESP detection is pending; otherwise one of: other, google, microsoft, yahoo, zoho
    "variables": object,           # Merge-tag values, e.g. {"company": "Acme"}
    "created_at": string,
    "updated_at": string
  }
}

Delete

DELETE /api/v1/campaigns/:campaign_id/leads/:id

Deletes a lead from a campaign.

Response: 204 No Content


Index

GET /api/v1/campaigns/:campaign_id/leads

Lists all leads for a specific campaign.

Query Parameters:

  • page_size: integer (optional, default: 30, max: 100) - Number of leads per page
  • page: integer (optional, default: 1) - Page number
  • search: string (optional) - Search term for leads
  • sort_by: string (optional) - Field to sort by
  • sort_order: string (optional, “asc” or “desc”) - Sort direction

Response:

{
  "data": [
    {
      "id": string,
      "email": string,
      "first_name": string,
      "last_name": string,
      "status": string,             # One of: uncontacted, contacted, replied
      "is_bounced": boolean,        # Whether the lead has bounced
      "is_unsubscribed": boolean,   # Whether the lead has unsubscribed
      "provider": string|null,      # null while ESP detection is pending; otherwise one of: other, google, microsoft, yahoo, zoho
      "variables": object,          # Merge-tag values, e.g. {"company": "Acme"}
      "created_at": string,
      "updated_at": string
    }
  ],
  "metadata": {
    "page": integer,
    "page_size": integer,
    "total_count": integer,
    "total_pages": integer
  }
}

Show

GET /api/v1/campaigns/:campaign_id/leads/:id

Shows details for a specific lead.

Response:

{
  "data": {
    "id": string,
    "email": string,
    "first_name": string,
    "last_name": string,
    "status": string,             # One of: uncontacted, contacted, replied
    "is_bounced": boolean,        # Whether the lead has bounced
    "is_unsubscribed": boolean,   # Whether the lead has unsubscribed
    "provider": string|null,      # null while ESP detection is pending; otherwise one of: other, google, microsoft, yahoo, zoho
    "variables": object,          # Merge-tag values, e.g. {"company": "Acme"}
    "created_at": string,
    "updated_at": string
  }
}

Sequence Endpoints

Create

POST /api/v1/campaigns/:campaign_id/sequences

Adds a new step to a campaign’s sequence. The new step is added to the end. A default empty variant is created alongside so the step is immediately editable; edit it via PATCH /campaigns/:id/variants/:variant_id or add more with POST /sequences/:id/variants.

delay_days is the gap to the next step. The new step is the last step, so its delay_days is returned as null. When you later add another step after it, the previous step’s delay_days becomes meaningful again.

Request:

{
  "delay_days": integer  # optional, defaults to 3
}

Response:

{
  "data": {
    "id": string,
    "step": integer,
    "delay_days": integer|null,  # null on the last step
    "variants": [
      {
        "id": string,
        "letter": "A",
        "subject": null,
        "body_html": string|null,  # null, or the unsubscribe-link block when the campaign has unsubscribe_option enabled
        "body_text": string|null,
        "enabled": true
      }
    ]
  }
}

Create variant

POST /api/v1/campaigns/:campaign_id/sequences/:id/variants

Adds a new variant to a sequence. Same content rules as PATCH /campaigns/:campaign_id/variants/:id: HTML is normalized, unsubscribe link is preserved, merge tags are validated.

Request:

{
  "subject": string,    # required, non-empty
  "body_html": string,  # optional
  "enabled": boolean    # optional, defaults to true
}

Response:

{
  "data": {
    "id": string,
    "subject": string,
    "body_html": string,
    "body_text": string,
    "enabled": boolean
  }
}

Delete

DELETE /api/v1/campaigns/:campaign_id/sequences/:id

Deletes a step and all of its variants. Subsequent steps shift up so step numbers stay contiguous (e.g. deleting step 2 from [1, 2, 3, 4] leaves [1, 2, 3]). Returns 204 on success.


Delete variant

DELETE /api/v1/campaigns/:campaign_id/variants/:variant_id

Deletes a variant. Returns 422 with cannot_delete_last_variant if it would leave the sequence with no enabled variants.


Index

GET /api/v1/campaigns/:campaign_id/sequences

Lists all sequences for a campaign with their variants, ordered by step.

delay_days is the number of days to wait after this step before sending the next step. The last step in a campaign has no next step, so its delay_days is null.

Response:

{
  "data": [
    {
      "id": string,
      "step": integer,
      "delay_days": integer|null,  # null on the last step
      "variants": [
        {
          "id": string,
          "letter": string,        # A, B, C, etc.
          "subject": string,
          "body_html": string,
          "body_text": string,
          "enabled": boolean
        }
      ]
    }
  ]
}

Update

PATCH /api/v1/campaigns/:campaign_id/sequences/:id

Updates a step’s delay_days.

Request:

{
  "delay_days": integer  # required
}

Response:

{
  "data": {
    "id": string,
    "step": integer,
    "delay_days": integer|null,  # null on the last step
    "variants": [
      {
        "id": string,
        "letter": string,        # A, B, C, etc.
        "subject": string,
        "body_html": string,
        "body_text": string,
        "enabled": boolean
      }
    ]
  }
}

Update variant

PATCH /api/v1/campaigns/:campaign_id/variants/:variant_id

Updates a sequence variant. Provide any combination of subject, body_html, and enabled. Omitted fields are unchanged.

Body HTML is restricted to a small allowlist (<b>, <i>, <u>, <a>, <img>) and normalized server-side, so the saved markup may differ slightly from what you submit. Other tags are unwrapped, unknown attributes are dropped.

Merge tags {{variable}}, {{variable|fallback}}, {{LOOM|column|fallback}}, and {{SPINTAX|opt1|opt2}} are supported in both subject and body_html. Use \{{...}} to include a literal {{ in the email. Unbalanced braces return 422 with the offset of the offending {{.

The last enabled variant in a sequence cannot be disabled. Campaigns with unsubscribe headers enabled keep an <a href="{{unsubscribe_url}}"> link in the body. If you remove it, it will be re-added.

Request:

{
  "subject": string,     # optional
  "body_html": string,   # optional
  "enabled": boolean     # optional
}

Response:

{
  "data": {
    "id": string,
    "subject": string,
    "body_html": string,
    "body_text": string,
    "enabled": boolean
  }
}

Need Help?

If you have questions about using the API or need additional endpoints for your specific use case, please contact our support team. We’re continually expanding our API capabilities based on user feedback.