STAGING · test accounts only · no production data
Skip to content
Home/API docs

Getting started

Create a key under Settings → Integrations (Scale and Enterprise plans), then send it in the Authorization: Bearer header from your server. Never embed keys in browser code or public repositories. Every request goes to https://api.xsenderapp.com/v1/public.

Authorization: Bearer xsk_live_…

Pagination

Collections return opaque cursors. Use limit from 1–100 and pass next_cursor unchanged.

Errors

Errors use { "code": "…", "message": "…" }. Common statuses are 400, 401, 402, 403, 404, 409, 413, 415, and 429. 429 rate limits always include Retry-After; temporary 409 or 503 responses may include it too.

Rate limits

Each key receives 600 reads and 120 writes per minute.

Scopes

Each key carries read or write access per resource. A write scope also grants reads of the same resource; nothing crosses resources.

Versioning

This is version 1.2.0. Fields are only ever added; nothing documented here is removed or renamed without a new major version.

A typical flow

Create a target list with your leads, create a campaign that uses it, activate it with confirmed: true, then read /campaigns/…/leads or listen to the webhook to see who was messaged.

Webhooks

Add one HTTPS endpoint under Settings → Integrations (or manage it over the API with the /webhooks endpoints below) and Xsender sends a message.sent event for every campaign message that goes out: the lead and their custom fields, the sending account, the campaign and step, and when. Opt in to message.failed to also hear when a message could not be sent, with the reason in data.message.status and data.message.error_code. Events carry no message text. Turning the webhook on also delivers your selected events from the last 30 days, oldest first. A webhook.test event is sent from the Settings test button. Reply events are not available yet.

{
  "id": "1f0c6a1e-5b7e-4a7a-9c2d-3f7c1e9a2b10",
  "api_version": "v1",
  "type": "message.sent",
  "created_at": "2026-09-05T12:00:00.000Z",
  "occurred_at": "2026-09-05T11:59:30.000Z",
  "data": {
    "contact": {
      "username": "lead_handle",
      "profile_url": "https://x.com/lead_handle",
      "custom_fields": { "company": "Example Ltd" }
    },
    "sender": {
      "x_user_id": "1234567890",
      "username": "sender_handle",
      "profile_url": "https://x.com/sender_handle"
    },
    "campaign": { "id": "cmp_123", "name": "Founders outreach" },
    "target_source": { "id": "tl_456", "name": "Seed-stage founders" },
    "message": {
      "id": "send_789",
      "direction": "outbound",
      "status": "sent",
      "attempted_at": "2026-09-05T11:59:30.000Z",
      "sent_at": "2026-09-05T11:59:30.000Z",
      "message_index": 0,
      "variant_index": 0,
      "variant_label": "First message",
      "attempts": 1
    }
  }
}

Verify the signature

Read X-Xsender-Timestamp, X-Xsender-Signature, X-Xsender-Event-Id, and X-Xsender-Event-Type. Compute HMAC-SHA256 over the exact bytes in ${timestamp}.${rawBody}, reject timestamps more than five minutes old, compare equal-length signatures in constant time, and deduplicate the event ID in durable storage. Xsender retries with at-least-once semantics.

const express = require('express');
const { createHmac, timingSafeEqual } = require('node:crypto');

const app = express();
const secret = process.env.XSENDER_WEBHOOK_SECRET;

app.post(
  '/webhooks/xsender',
  express.raw({ type: 'application/json' }),
  async (request, response) => {
    const timestamp = request.get('X-Xsender-Timestamp') || '';
    const signature = request.get('X-Xsender-Signature') || '';
    const eventId = request.get('X-Xsender-Event-Id') || '';
    const eventType = request.get('X-Xsender-Event-Type') || '';
    const timestampSeconds = Number(timestamp);

    if (
      !secret ||
      !eventId ||
      !Number.isInteger(timestampSeconds) ||
      Math.abs(Math.floor(Date.now() / 1000) - timestampSeconds) > 300
    ) {
      return response.sendStatus(401);
    }

    const receivedHex = /^v1=([a-f0-9]{64})$/.exec(signature)?.[1];
    if (!receivedHex) return response.sendStatus(401);

    const expected = Buffer.from(
      createHmac('sha256', secret)
        .update(`${timestamp}.`)
        .update(request.body)
        .digest('hex'),
      'hex'
    );
    const received = Buffer.from(receivedHex, 'hex');
    if (
      expected.length !== received.length ||
      !timingSafeEqual(expected, received)
    ) {
      return response.sendStatus(401);
    }

    // Atomically record eventId in durable storage before processing.
    // If it already exists, return 204 without processing it again.
    const event = JSON.parse(request.body.toString('utf8'));
    request.app.emit('xsender.webhook', { eventId, eventType, event });
    return response.sendStatus(204);
  }
);

Inbox

The inbox holds every DM thread across your connected accounts, with replies, tags, notes and read state, and runs on its own service. Call GET /inbox/session with a key that has the inbox:read or inbox:write scope; the answer carries base_url and a token that is valid for one hour. Send that token as Authorization: Bearer to the endpoints below, and ask for a new session when it expires. A read-only session (a key without inbox:write) answers every GET and refuses everything else with 403 read_only_session. Every inbox call goes to base_url on the inbox host, never to api.xsenderapp.com, and an API-key session works from any runtime, whatever Origin header it sends. Error bodies carry an error code and a message that says what to do.

# 1. Open a session (valid for one hour)
curl https://api.xsenderapp.com/v1/public/inbox/session \
  -H "Authorization: Bearer xsk_live_…"
# → { "base_url": "https://inbox.xsenderapp.com/w/WORKSPACE_ID/api",
#     "token": "…", "expires_at": "…", "access": "write" }

# 2. Read the unread threads with it
curl "https://inbox.xsenderapp.com/w/WORKSPACE_ID/api/conversations?filter=unread&limit=100" \
  -H "Authorization: Bearer INBOX_TOKEN"

# 3. Reply, then set the lead's tags
curl -X POST "https://inbox.xsenderapp.com/w/WORKSPACE_ID/api/conversations/THREAD_KEY/reply" \
  -H "Authorization: Bearer INBOX_TOKEN" -H "Content-Type: application/json" \
  -d '{"text":"awesome here you go","client_id":"crm:lead-42:step-2"}'
curl -X PUT "https://inbox.xsenderapp.com/w/WORKSPACE_ID/api/leads/PEER_ID/tags" \
  -H "Authorization: Bearer INBOX_TOKEN" -H "Content-Type: application/json" \
  -d '{"tag_ids":["tag_abc"]}'

Endpoints under base_url

  • GET /bootstrap?filter=unread&limit=100 accounts (online, paused), the tag catalog with a lead count per tag, and the first page of threads, in one call.
  • GET /conversations?filter=unread|all&account=&tags=id,id&q=&limit=&before=&before_key= threads newest first; tags= matches leads carrying any of those tags.
  • GET /conversations/{key} one thread row.
  • GET /conversations/{key}/messages?limit=&before=&before_id= the thread, oldest first within the page, plus its pending outbox.
  • POST /conversations/{key}/reply { text, client_id? } — one message from the account that owns the thread.
  • POST /conversations/{key}/reply-sequence { parts: [text, …], client_ids? } — up to 5 messages in order with a human-like gap.
  • POST /conversations/{key}/outbox/{id}/retry retry a failed send in place.
  • DELETE /conversations/{key}/outbox/{id} take back a send the browser has not taken yet.
  • POST /conversations/{key}/read mark read.
  • POST /conversations/{key}/unread mark unread (and pin it in the Unread view).
  • POST /send { account_id, recipient_id, text, client_id? } — start a thread with any X user.
  • GET /tags the workspace tag catalog, with a lead count per tag.
  • POST /tags { name, color? } — create a tag, or get the existing one with that name.
  • PATCH /tags/{id} { name?, color? } — rename or recolour.
  • DELETE /tags/{id} delete a tag and remove it from every lead.
  • GET /leads/{peer_id} profile, tags, note and every thread with this person.
  • PATCH /leads/{peer_id} { note? } — the note shown in the lead panel (up to 4,000 characters).
  • PUT /leads/{peer_id}/tags { tag_ids: [...] } — set the complete tag list in one call.
  • PUT /leads/{peer_id}/tags/{id} add one tag.
  • DELETE /leads/{peer_id}/tags/{id} remove one tag.
  • GET /state accounts and their online state, thread and unread counts.

How the pieces fit

A thread row carries key, account_id, account_username, peer_id, peer_username, last_text, last_at, last_direction (inbound or outbound), unread, pending_sends, last_automated (1 when the newest message was a campaign starter, 2 for a campaign follow-up), the lead's tags and note. Lists are keyset-paged: pass next.before and next.before_key back until next is null. Tags and notes belong to the person (peer_id), so the same lead across two of your accounts shares them. Sending a reply marks the thread read. Give every send a client_id: a repeat of the same id inside 24 hours is ignored, so a retried job cannot send twice. Tags are referenced by id; POST /tags with a name that already exists returns the existing tag.

Campaigns

GET/campaignscampaigns:read

List campaigns

Parameters

NameLocationTypeRequiredDetails
limitqueryintegerNoDefault: 50 · Minimum: 1 · Maximum: 100
cursorquerystringNo—

Responses

200Campaign pageCampaignPage
400API errorError
401API errorError
402API errorError
403API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/campaigns' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
POST/campaignscampaigns:write

Create a draft campaign

Request body Required

application/json · CampaignCreateInput

  • At least 1 property must be provided.
PropertyTypeRequiredRules
namestringYes—
descriptionstringNo—
workMinsarray<unspecified>NoMinimum items: 2 · Maximum items: 2
perDayintegerNoMinimum: 0 · Maximum: 500
sendDaysarray<string>No—
targetListIdsarray<string>No—
accountIdsarray<string>No—
sequencearray<object>No—

Responses

201CampaignCampaign
400API errorError
401API errorError
402API errorError
403API errorError
413Request body exceeds the 256 KB limit.Error
415Content-Type must be application/json for requests with a body.Error
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request POST \
  --url 'https://api.xsenderapp.com/v1/public/campaigns' \
  --header 'Authorization: Bearer $XSENDER_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"name":"New campaign","targetListIds":[],"accountIds":[]}'
GET/campaigns/{campaign_id}campaigns:read

Get a campaign

Parameters

NameLocationTypeRequiredDetails
campaign_idpathstringYes—

Responses

200CampaignCampaign
401API errorError
402API errorError
403API errorError
404API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/campaigns/CAMPAIGN_ID' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
PATCH/campaigns/{campaign_id}campaigns:write

Update a campaign

Parameters

NameLocationTypeRequiredDetails
campaign_idpathstringYes—

Request body Required

application/json · CampaignUpdateInput

  • At least 1 property must be provided.
PropertyTypeRequiredRules
namestringNo—
descriptionstringNo—
workMinsarray<unspecified>NoMinimum items: 2 · Maximum items: 2
perDayintegerNoMinimum: 0 · Maximum: 500
sendDaysarray<string>No—
targetListIdsarray<string>No—
accountIdsarray<string>No—
sequencearray<object>No—

Responses

200CampaignCampaign
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
413Request body exceeds the 256 KB limit.Error
415Content-Type must be application/json for requests with a body.Error
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request PATCH \
  --url 'https://api.xsenderapp.com/v1/public/campaigns/CAMPAIGN_ID' \
  --header 'Authorization: Bearer $XSENDER_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"name":"Updated campaign name"}'
DELETE/campaigns/{campaign_id}campaigns:write

Delete a campaign

Parameters

NameLocationTypeRequiredDetails
campaign_idpathstringYes—

Responses

200Action acceptedobject
401API errorError
402API errorError
403API errorError
404API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request DELETE \
  --url 'https://api.xsenderapp.com/v1/public/campaigns/CAMPAIGN_ID' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
POST/campaigns/{campaign_id}/activatecampaigns:write

Activate a confirmed campaign

Parameters

NameLocationTypeRequiredDetails
campaign_idpathstringYes—

Request body Optional

application/json · object

PropertyTypeRequiredRules
confirmedbooleanNo—

Responses

200CampaignCampaign
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
409API errorError
413Request body exceeds the 256 KB limit.Error
415Content-Type must be application/json for requests with a body.Error
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request POST \
  --url 'https://api.xsenderapp.com/v1/public/campaigns/CAMPAIGN_ID/activate' \
  --header 'Authorization: Bearer $XSENDER_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"confirmed":true}'
POST/campaigns/{campaign_id}/pausecampaigns:write

Pause a campaign

Parameters

NameLocationTypeRequiredDetails
campaign_idpathstringYes—

Responses

200CampaignCampaign
401API errorError
402API errorError
403API errorError
404API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request POST \
  --url 'https://api.xsenderapp.com/v1/public/campaigns/CAMPAIGN_ID/pause' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
GET/campaigns/{campaign_id}/leadscampaigns:read

List who a campaign has messaged

One row per finished send attempt, newest first, from the same ledger the dashboard counts. Use outcome=sent for successful sends only.

Parameters

NameLocationTypeRequiredDetails
campaign_idpathstringYes—
sincequerystringNoOnly sends that finished at or after this ISO 8601 date-time.
untilquerystringNoOnly sends that finished at or before this ISO 8601 date-time.
outcomequerystringNoOnly outcomes of this kind. · Allowed: sent, skipped, rate_limited, blocked, account_locked, failed, expired, auth_required, cancelled
limitqueryintegerNoDefault: 50 · Minimum: 1 · Maximum: 100
cursorquerystringNo—

Responses

200Lead outcome pageLeadOutcomePage
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/campaigns/CAMPAIGN_ID/leads' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'

Target lists

GET/target-liststargets:read

List target lists

Parameters

NameLocationTypeRequiredDetails
limitqueryintegerNoDefault: 50 · Minimum: 1 · Maximum: 100
cursorquerystringNo—

Responses

200Target-list pageTargetListPage
400API errorError
401API errorError
402API errorError
403API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/target-lists' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
POST/target-liststargets:write

Create a target list

Request body Required

application/json · TargetListCreateInput

  • At least 1 property must be provided.
  • When `variableSchema` is provided, also requires `leads`.
  • When any `leads[].variables` has at least 1 property, also requires `variableSchema` (Minimum items: 1).
  • Requires at least one of: `handles` or `leads`.
PropertyTypeRequiredRules
namestringYes—
handlesarray<string>No—
leadsarray<Lead>No—
variableSchemaarray<TargetVariableField>No—

Responses

201Target listTargetList
400API errorError
401API errorError
402API errorError
403API errorError
413Request body exceeds the 24 MB limit.Error
415Content-Type must be application/json for requests with a body.Error
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request POST \
  --url 'https://api.xsenderapp.com/v1/public/target-lists' \
  --header 'Authorization: Bearer $XSENDER_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"name":"Prospects","variableSchema":[{"key":"company","label":"Company"}],"leads":[{"username":"example","variables":{"company":"Acme"}}]}'
GET/target-lists/{target_list_id}targets:read

Get a target list

Parameters

NameLocationTypeRequiredDetails
target_list_idpathstringYes—

Responses

200Target listTargetList
401API errorError
402API errorError
403API errorError
404API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/target-lists/TARGET_LIST_ID' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
PATCH/target-lists/{target_list_id}targets:write

Update a target list

Parameters

NameLocationTypeRequiredDetails
target_list_idpathstringYes—

Request body Required

application/json · TargetListUpdateInput

  • At least 1 property must be provided.
  • Requires at least one of: `name` or `handles` or `leads`.
  • When `variableSchema` is provided, also requires `leads`.
  • When any `leads[].variables` has at least 1 property, also requires `variableSchema` (Minimum items: 1).
PropertyTypeRequiredRules
namestringNo—
handlesarray<string>No—
leadsarray<Lead>No—
variableSchemaarray<TargetVariableField>No—

Responses

200Target listTargetList
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
413Request body exceeds the 24 MB limit.Error
415Content-Type must be application/json for requests with a body.Error
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request PATCH \
  --url 'https://api.xsenderapp.com/v1/public/target-lists/TARGET_LIST_ID' \
  --header 'Authorization: Bearer $XSENDER_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"name":"Qualified prospects"}'
DELETE/target-lists/{target_list_id}targets:write

Delete a target list

Parameters

NameLocationTypeRequiredDetails
target_list_idpathstringYes—

Responses

200Action acceptedobject
401API errorError
402API errorError
403API errorError
404API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request DELETE \
  --url 'https://api.xsenderapp.com/v1/public/target-lists/TARGET_LIST_ID' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
GET/target-lists/{target_list_id}/leadstargets:read

List target-list leads and custom variables

Parameters

NameLocationTypeRequiredDetails
target_list_idpathstringYes—
limitqueryintegerNoDefault: 50 · Minimum: 1 · Maximum: 100
cursorquerystringNo—

Responses

200Lead pageLeadPage
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/target-lists/TARGET_LIST_ID/leads' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
POST/target-lists/{target_list_id}/leadstargets:write

Add leads to a target list

Adds new usernames to the end of the list. Usernames already on the list and leads this workspace has already messaged are skipped and counted in the result. Campaigns using the list keep running. Send one append at a time per list: two appends that overlap in time can each miss the other’s leads.

Parameters

NameLocationTypeRequiredDetails
target_list_idpathstringYes—

Request body Required

application/json · TargetListLeadsAppendInput

  • At least 1 property must be provided.
  • Requires at least one of: `handles` or `leads`.
  • When `variableSchema` is provided, also requires `leads`.
PropertyTypeRequiredRules
handlesarray<string>No—
leadsarray<Lead>No—
variableSchemaarray<TargetVariableField>NoDeclares the variable keys used in leads[].variables. New keys are added to the list’s existing schema.

Responses

200Leads appendedTargetListLeadsAppendResult
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
409API errorError
413Request body exceeds the 24 MB limit.Error
415Content-Type must be application/json for requests with a body.Error
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request POST \
  --url 'https://api.xsenderapp.com/v1/public/target-lists/TARGET_LIST_ID/leads' \
  --header 'Authorization: Bearer $XSENDER_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"variableSchema":[{"key":"company","label":"Company"}],"leads":[{"username":"new_lead","variables":{"company":"Globex"}}]}'

Accounts

GET/accountsaccounts:read

List connected X accounts

Parameters

NameLocationTypeRequiredDetails
limitqueryintegerNoDefault: 50 · Minimum: 1 · Maximum: 100
cursorquerystringNo—

Responses

200Connected X account pageConnectedAccountPage
400API errorError
401API errorError
402API errorError
403API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/accounts' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
PATCH/accounts/{account_id}accounts:write

Pause or resume a connected X account

Returns the account with its new pause state, the same shape as the list.

Parameters

NameLocationTypeRequiredDetails
account_idpathstringYes—

Request body Required

application/json · AccountPauseInput

PropertyTypeRequiredRules
pausedbooleanYes—

Responses

200Connected accountConnectedAccount
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
413Request body exceeds the 256 KB limit.Error
415Content-Type must be application/json for requests with a body.Error
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request PATCH \
  --url 'https://api.xsenderapp.com/v1/public/accounts/ACCOUNT_ID' \
  --header 'Authorization: Bearer $XSENDER_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"paused":true}'

Settings

GET/settingssettings:read

Get workspace settings

Responses

200SettingsSettings
401API errorError
402API errorError
403API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/settings' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
PUT/settingssettings:write

Update user-editable workspace settings

Request body Required

application/json · SettingsUpdate

  • At least 1 property must be provided.
PropertyTypeRequiredRules
timezonestringNo—
blackliststringNo—

Responses

200SettingsSettings
400API errorError
401API errorError
402API errorError
403API errorError
413Request body exceeds the 2 MB limit.Error
415Content-Type must be application/json for requests with a body.Error
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request PUT \
  --url 'https://api.xsenderapp.com/v1/public/settings' \
  --header 'Authorization: Bearer $XSENDER_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"timezone":"Europe/Vilnius"}'

Specification

GET/openapi.json

Download the public OpenAPI document

Responses

200OpenAPI 3.1 document
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/openapi.json'

Metrics

GET/metricsanalytics:read

Workspace analytics totals

The same analytics the dashboard shows: daily send activity, per-campaign, per-account and per-variant rollups, top blockers, and engagement. Defaults to the last 31 days. Pass range=all for all-time, or from/to for a custom window (the two cannot be combined). Day buckets use the workspace timezone.

Parameters

NameLocationTypeRequiredDetails
rangequerystringNoSet to "all" for all-time totals. Omit for the default last-31-days window, or use from/to instead. Cannot be combined with from or to. · Allowed: all
fromquerystringNoStart of the window (ISO 8601 date-time), inclusive. Defaults to 31 days ago. Cannot be combined with range=all.
toquerystringNoEnd of the window (ISO 8601 date-time), inclusive. Defaults to now. Cannot be combined with range=all.

Responses

200Workspace analytics totalsMetrics
400API errorError
401API errorError
402API errorError
403API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
503API errorError
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/metrics' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
GET/eventscampaigns:read

Stream send outcomes across every campaign

Every campaign's finished sends in time order (oldest first), for syncing into your own pipeline. Page with cursor to pull only what is new since your last run: the keyset cursor never skips or repeats a row even while new sends are landing. Narrow with since/until or a single outcome.

Parameters

NameLocationTypeRequiredDetails
sincequerystringNoOnly sends that finished at or after this ISO 8601 date-time.
untilquerystringNoOnly sends that finished at or before this ISO 8601 date-time.
outcomequerystringNoOnly outcomes of this kind. · Allowed: sent, skipped, rate_limited, blocked, account_locked, failed, expired, auth_required, cancelled
limitqueryintegerNoDefault: 50 · Minimum: 1 · Maximum: 100
cursorquerystringNo—

Responses

200Send outcome event pageOutcomeEventPage
400API errorError
401API errorError
402API errorError
403API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/events' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'

Webhooks

GET/webhooks/endpointwebhooks:read

Get the webhook endpoint

The workspace's single webhook endpoint, or null if none exists yet.

Responses

200OKWebhookEndpointState
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
409API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/webhooks/endpoint' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
POST/webhooks/endpointwebhooks:write

Create the webhook endpoint

Creates the workspace's single endpoint (409 if one already exists) and returns the signing secret once. Send a successful test, then enable it, before deliveries flow.

Request body Required

application/json · WebhookEndpointCreateInput

PropertyTypeRequiredRules
urlstringYesA public HTTPS endpoint on port 443.
event_typesarray<string>NoWhich events to deliver. Defaults to ["message.sent"].

Responses

201CreatedWebhookEndpointState
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
409API errorError
413Request body exceeds the 256 KB limit.Error
415Content-Type must be application/json for requests with a body.Error
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request POST \
  --url 'https://api.xsenderapp.com/v1/public/webhooks/endpoint' \
  --header 'Authorization: Bearer $XSENDER_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"url":"https://example.com/hooks/xsender","event_types":["message.sent"]}'
PATCH/webhooks/endpointwebhooks:write

Change URL, events, or disable

Provide exactly one change: a new url (which requires re-verification), a new event selection, or enabled=false to stop delivery.

Request body Required

application/json · WebhookEndpointUpdateInput

PropertyTypeRequiredRules
urlstringNo—
event_typesarray<string>No—
enabledbooleanNoOnly false is accepted, to disable delivery.

Responses

200OKWebhookEndpointState
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
409API errorError
413Request body exceeds the 256 KB limit.Error
415Content-Type must be application/json for requests with a body.Error
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request PATCH \
  --url 'https://api.xsenderapp.com/v1/public/webhooks/endpoint' \
  --header 'Authorization: Bearer $XSENDER_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{"event_types":["message.sent","message.failed"]}'
DELETE/webhooks/endpointwebhooks:write

Delete the webhook endpoint

Responses

200OKWebhookOk
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
409API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request DELETE \
  --url 'https://api.xsenderapp.com/v1/public/webhooks/endpoint' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
POST/webhooks/endpoint/testwebhooks:write

Send a signed test event

Delivers a signed webhook.test event. A success verifies the endpoint so it can be enabled.

Responses

200OKWebhookTestResult
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
409API errorError
422API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request POST \
  --url 'https://api.xsenderapp.com/v1/public/webhooks/endpoint/test' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
POST/webhooks/endpoint/enablewebhooks:write

Enable delivery

Enables delivery (requires a prior successful test) and backfills your selected events from the last 30 days, oldest first.

Responses

200OKWebhookEndpointState
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
409API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request POST \
  --url 'https://api.xsenderapp.com/v1/public/webhooks/endpoint/enable' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
POST/webhooks/endpoint/rotate-secretwebhooks:write

Rotate the signing secret

Issues a new signing secret (returned once) and disables delivery until the endpoint is tested and enabled again.

Responses

200OKWebhookEndpointState
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
409API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request POST \
  --url 'https://api.xsenderapp.com/v1/public/webhooks/endpoint/rotate-secret' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
GET/webhooks/deliverieswebhooks:read

List recent deliveries

The most recent delivery attempts, newest first, with their status and last result.

Parameters

NameLocationTypeRequiredDetails
limitqueryintegerNoDefault: 50 · Minimum: 1 · Maximum: 100

Responses

200OKWebhookDeliveryList
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
409API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/webhooks/deliveries' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'
POST/webhooks/deliveries/{delivery_id}/retrywebhooks:write

Retry a failed delivery

Re-queues a failed delivery for another attempt. The endpoint must be enabled.

Parameters

NameLocationTypeRequiredDetails
delivery_idpathstringYesA webhook delivery id, from the deliveries list.

Responses

200OKWebhookRetryResult
400API errorError
401API errorError
402API errorError
403API errorError
404API errorError
409API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
curl --request POST \
  --url 'https://api.xsenderapp.com/v1/public/webhooks/deliveries/{delivery_id}/retry' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'

Inbox

GET/inbox/sessioninbox:read

Open an inbox session

The inbox runs on its own service. This call turns the API key into a session for it: send the returned token as Authorization: Bearer to the endpoints under base_url for the next hour, then call here again. With inbox:read only, the session is read-only (every GET works, anything else answers 403 read_only_session); with inbox:write it can also reply, tag, add notes and mark threads read or unread. Every inbox call goes to base_url on the inbox host, never to this API host, and an API-key session is accepted from any runtime whatever Origin header it sends. The inbox endpoints, their fields and bodies are described in the Inbox section of the reference.

Responses

200Inbox sessionInboxSession
401API errorError
402API errorError
403API errorError
429Per-key rate limit exceeded. Retry-After contains seconds.Error
503API errorError
curl --request GET \
  --url 'https://api.xsenderapp.com/v1/public/inbox/session' \
  --header 'Authorization: Bearer $XSENDER_API_KEY'

Schemas

The canonical specification includes reusable schemas for campaigns, target lists, lead outcomes, connected accounts, settings, cursors, and the shared error envelope.

Error

object · Required: code, message

PropertyTypeRequiredRules
codestringYes—
messagestringYes—

Settings

object · Required: timezone, blacklist

PropertyTypeRequiredRules
timezonestringYes—
blackliststringYesNewline-delimited X usernames.

SettingsUpdate

object

  • At least 1 property must be provided.
PropertyTypeRequiredRules
timezonestringNo—
blackliststringNo—

Campaign

object · Required: id, name, status, createdAt, targetListIds, accountIds, sequence, messagesSent, contactedCount

PropertyTypeRequiredRules
idstringYes—
namestringYes—
descriptionstringNo—
statusstringYesAllowed: draft, active, paused, completed
statusReasonstring | nullNoWhy the campaign is in its current status, e.g. user_paused, completed_all_leads.
statusChangedAtstring | nullNo—
createdAtstringYes—
targetListIdsarray<string>Yes—
accountIdsarray<string>YesX user ids of the sending accounts.
sequencearray<SequenceStep>Yes—
workMinsintegerNoMinutes per day the campaign sends.
perDayintegerNoMinimum: 0 · Maximum: 500 · Daily send target across all accounts. Ignored when volumeMode is managed.
sendDaysarray<string>No—
volumeModestringNoAllowed: manual, managed · managed lets Xsender pace sends within each account’s safe limit.
messagesSentintegerYesSuccessful sends so far, all time.
contactedCountintegerYesLeads the campaign has claimed or messaged.
nextSendAtstring | nullNoNext planned send, null when nothing is scheduled.
confirmationRequiredbooleanNotrue until the current content revision has been activated with confirmed: true.

CampaignUpdateInput

object

  • At least 1 property must be provided.
PropertyTypeRequiredRules
namestringNo—
descriptionstringNo—
workMinsarray<unspecified>NoMinimum items: 2 · Maximum items: 2
perDayintegerNoMinimum: 0 · Maximum: 500
sendDaysarray<string>No—
targetListIdsarray<string>No—
accountIdsarray<string>No—
sequencearray<object>No—

CampaignCreateInput

CampaignUpdateInput + object · Required: name

  • At least 1 property must be provided.
PropertyTypeRequiredRules
namestringYes—
descriptionstringNo—
workMinsarray<unspecified>NoMinimum items: 2 · Maximum items: 2
perDayintegerNoMinimum: 0 · Maximum: 500
sendDaysarray<string>No—
targetListIdsarray<string>No—
accountIdsarray<string>No—
sequencearray<object>No—

TargetList

object · Required: id, name, count, variableSchema, createdAt

PropertyTypeRequiredRules
idstringYes—
namestringYes—
countintegerYesUsable leads on the list.
handlesarray<string>NoLead usernames. Present on single-list reads and writes; use /leads to page through variables.
variableSchemaarray<TargetVariableField>Yes—
createdAtstringYes—

TargetVariableField

object · Required: key, label

PropertyTypeRequiredRules
keystringYesPattern: ^[A-Za-z][A-Za-z0-9]*$
labelstringYesMaximum length: 200

TargetListUpdateInput

object

  • At least 1 property must be provided.
  • Requires at least one of: `name` or `handles` or `leads`.
  • When `variableSchema` is provided, also requires `leads`.
  • When any `leads[].variables` has at least 1 property, also requires `variableSchema` (Minimum items: 1).
PropertyTypeRequiredRules
namestringNo—
handlesarray<string>No—
leadsarray<Lead>No—
variableSchemaarray<TargetVariableField>No—

TargetListCreateInput

TargetListUpdateInput + object · Required: name

  • At least 1 property must be provided.
  • When `variableSchema` is provided, also requires `leads`.
  • When any `leads[].variables` has at least 1 property, also requires `variableSchema` (Minimum items: 1).
  • Requires at least one of: `handles` or `leads`.
PropertyTypeRequiredRules
namestringYes—
handlesarray<string>No—
leadsarray<Lead>No—
variableSchemaarray<TargetVariableField>No—

Lead

object · Required: username, variables

PropertyTypeRequiredRules
usernamestringYes—
variablesobjectYes—

AccountPauseInput

object · Required: paused

PropertyTypeRequiredRules
pausedbooleanYes—

ConnectedAccount

object · Required: user_id, username, installation_id, online, paused, busy, login_state, safety_cooldown_until

PropertyTypeRequiredRules
user_idstringYes—
usernamestring | nullYes—
installation_idstringYes—
onlinebooleanYestrue while the extension for this account has checked in during the last two minutes.
pausedbooleanYes—
busybooleanYestrue while a send is in progress on this account.
login_statestringYeslogged_in, logged_out or unknown as last reported by the extension.
safety_cooldown_untilstring | nullYesSet while X asked this account to slow down; sends resume after it.

ConnectedAccountPage

object · Required: data, next_cursor

PropertyTypeRequiredRules
dataarray<ConnectedAccount>Yes—
next_cursorstring | nullYes—

CampaignPage

object · Required: data, next_cursor

PropertyTypeRequiredRules
dataarray<Campaign>Yes—
next_cursorstring | nullYes—

TargetListPage

object · Required: data, next_cursor

PropertyTypeRequiredRules
dataarray<TargetList>Yes—
next_cursorstring | nullYes—

LeadPage

object · Required: data, next_cursor

PropertyTypeRequiredRules
dataarray<Lead>Yes—
next_cursorstring | nullYes—

SequenceVariant

object · Required: text

PropertyTypeRequiredRules
textstringYesMessage text. Placeholders like {{company}} are filled from the lead’s custom fields.

SequenceStep

object · Required: label, variants

PropertyTypeRequiredRules
idstringNo—
labelstringYes"First message" or "Follow-up N"; assigned by position.
delayobjectNoFollow-up wait after the previous step (follow-ups only).
variantsarray<SequenceVariant>YesA/B variants for this step; one is picked per lead.

LeadOutcome

object · Required: username, outcome, at, message_index, variant_index, attempts

PropertyTypeRequiredRules
usernamestringYes—
outcomestringYesAllowed: sent, skipped, rate_limited, blocked, account_locked, failed, expired, auth_required, cancelled · sent is the only success. skipped means X reported the lead cannot be messaged.
error_codestring | nullNoMachine-readable reason for a non-sent outcome.
atstringYesWhen the attempt finished.
account_idstring | nullNoX user id of the sending account (matches ConnectedAccount.user_id).
message_indexintegerYes0 for the first message, 1 for the first follow-up, and so on.
variant_indexintegerYes—
attemptsintegerYesMinimum: 1

LeadOutcomePage

object · Required: data, next_cursor

PropertyTypeRequiredRules
dataarray<LeadOutcome>Yes—
next_cursorstring | nullYes—

TargetListLeadsAppendInput

object

  • At least 1 property must be provided.
  • Requires at least one of: `handles` or `leads`.
  • When `variableSchema` is provided, also requires `leads`.
PropertyTypeRequiredRules
handlesarray<string>No—
leadsarray<Lead>No—
variableSchemaarray<TargetVariableField>NoDeclares the variable keys used in leads[].variables. New keys are added to the list’s existing schema.

TargetListLeadsAppendResult

object · Required: target_list, added, skipped_duplicates, skipped_contacted

PropertyTypeRequiredRules
target_listTargetList | nullYesThe list after the change; null when nothing was added.
addedintegerYes—
skipped_duplicatesintegerYesUsernames already on the list.
skipped_contactedintegerYesUsernames this workspace has already messaged from any campaign.

MetricsRange

object · Required: all_time, from, to

PropertyTypeRequiredRules
all_timebooleanYes—
fromstring | nullYesStart of the window the totals cover, or null for all-time.
tostring | nullYesEnd of the window the totals cover, or null for open-ended.

MetricsMessaging

object · Required: sent, skipped, rate_limited, blocked, account_locked, auth_required, failed, expired, cancelled, terminal, deliverable, unique_recipients, retry_attempts

PropertyTypeRequiredRules
sentintegerYesSuccessful sends (the only success).
skippedintegerYes—
rate_limitedintegerYes—
blockedintegerYes—
account_lockedintegerYes—
auth_requiredintegerYes—
failedintegerYes—
expiredintegerYes—
cancelledintegerYes—
terminalintegerYesAttempts that reached a final outcome (sent plus every failure kind).
deliverableintegerYesTerminal attempts excluding cancelled ones.
unique_recipientsintegerYes—
retry_attemptsintegerYes—

MetricsEngagement

object · Required: contacted, replied

PropertyTypeRequiredRules
contactedintegerYesDistinct recipients messaged in the window.
repliedintegerYesContacted recipients who replied. Requires reply tracking.

MetricsDailyPoint

object · Required: day, sent, failed, skipped, total

PropertyTypeRequiredRules
daystringYesCalendar day (YYYY-MM-DD) in the workspace timezone.
sentintegerYes—
failedintegerYes—
skippedintegerYes—
totalintegerYes—

MetricsCampaign

object · Required: campaign_id, sent, skipped, rate_limited, blocked, failed, attempted, contacted, replies

PropertyTypeRequiredRules
campaign_idstringYes—
sentintegerYes—
skippedintegerYes—
rate_limitedintegerYes—
blockedintegerYes—
failedintegerYes—
attemptedintegerYes—
contactedintegerYes—
repliesintegerYes—

MetricsAccount

object · Required: account_id, sent, failed, risk_stops, contacted, replied

PropertyTypeRequiredRules
account_idstringYesX user id of the sending account (matches ConnectedAccount.user_id).
sentintegerYes—
failedintegerYes—
risk_stopsintegerYesSends stopped for safety (blocks, rate limits, locks).
contactedintegerYes—
repliedintegerYes—

MetricsVariant

object · Required: campaign_id, message_index, variant_index, sent, replies

PropertyTypeRequiredRules
campaign_idstringYes—
message_indexintegerYes0 for the first message, 1 for the first follow-up, and so on.
variant_indexintegerYes—
sentintegerYes—
repliesintegerYes—

MetricsBlocker

object · Required: code, count

PropertyTypeRequiredRules
codestringYesMachine-readable reason sends did not complete.
countintegerYes—

Metrics

object · Required: range, messaging, engagement, daily, campaigns, accounts, variants, blockers, pace

PropertyTypeRequiredRules
rangeMetricsRangeYes—
messagingMetricsMessagingYes—
engagementMetricsEngagementYes—
dailyarray<MetricsDailyPoint>YesPer-day send activity, oldest first.
campaignsarray<MetricsCampaign>YesPer-campaign outcome totals.
accountsarray<MetricsAccount>YesPer-sending-account outcome totals.
variantsarray<MetricsVariant>YesPer-message, per-variant sends and replies for A/B comparison.
blockersarray<MetricsBlocker>YesThe reasons, with counts, that sends did not complete.
pacearray<MetricsCampaignPace>YesHow long each campaign's leads last: the numbers behind the dashboard's Leads left card. Not bound to the range: remaining_leads counts against everything the campaign has ever contacted, and the pace fields always cover the last seven days. Days of leads left ≈ remaining_leads ÷ (contacts_last_7_days ÷ send_days_last_7_days).

OutcomeEvent

object · Required: username, outcome, at, account_id, message_index, variant_index, attempts, campaign_id

PropertyTypeRequiredRules
usernamestringYes—
outcomestringYesAllowed: sent, skipped, rate_limited, blocked, account_locked, failed, expired, auth_required, cancelled · sent is the only success. skipped means X reported the lead cannot be messaged.
error_codestring | nullNoMachine-readable reason for a non-sent outcome.
atstringYesWhen the attempt finished.
account_idstring | nullYesX user id of the sending account (matches ConnectedAccount.user_id).
message_indexintegerYes0 for the first message, 1 for the first follow-up, and so on.
variant_indexintegerYes—
attemptsintegerYesMinimum: 1
campaign_idstring | nullYesThe campaign this send belongs to (matches Campaign.id).

OutcomeEventPage

object · Required: data, next_cursor

PropertyTypeRequiredRules
dataarray<OutcomeEvent>Yes—
next_cursorstring | nullYesOpaque cursor for the next page. Pass it back as cursor to resume exactly after the last event; null when caught up.

WebhookEndpoint

object · Required: id, url_masked, event_types, status, verified_at, enabled_at, disabled_at, backfill_started_at, backfill_completed_at, last_tested_at, last_test_http_status, created_at, updated_at

PropertyTypeRequiredRules
idstringYes—
url_maskedstringYesThe endpoint URL with its path hidden. The full URL is never returned.
event_typesarray<string>YesThe events this endpoint delivers.
statusstringYesAllowed: pending, backfilling, enabled, disabled
verified_atstring | nullYesWhen a test last succeeded against the current URL.
enabled_atstring | nullYes—
disabled_atstring | nullYes—
backfill_started_atstring | nullYes—
backfill_completed_atstring | nullYes—
last_tested_atstring | nullYes—
last_test_http_statusinteger | nullYesHTTP status the endpoint returned to the last test.
created_atstringYes—
updated_atstringYes—

WebhookEndpointState

object · Required: available, endpoint

PropertyTypeRequiredRules
availablebooleanYes—
endpointWebhookEndpoint | nullYes—
signing_secretstringNoReturned once by create and rotate-secret. Store it: it signs deliveries and is never shown again.
enabledbooleanNo—
backfilled_countintegerNo—
pending_countintegerNo—
enabled_atstring | nullNo—

WebhookDelivery

object · Required: id, event_type, status, occurred_at, attempt_count, next_attempt_at, last_attempt_at, delivered_at, failed_at, last_http_status, last_error_code, created_at

PropertyTypeRequiredRules
idstringYes—
event_typestringYes—
statusstringYesAllowed: pending, processing, delivered, failed
occurred_atstringYes—
attempt_countintegerYes—
next_attempt_atstring | nullYes—
last_attempt_atstring | nullYes—
delivered_atstring | nullYes—
failed_atstring | nullYes—
last_http_statusinteger | nullYes—
last_error_codestring | nullYes—
created_atstringYes—

WebhookDeliveryList

object · Required: deliveries

PropertyTypeRequiredRules
deliveriesarray<WebhookDelivery>Yes—

WebhookTestResult

object · Required: ok, http_status, endpoint

PropertyTypeRequiredRules
okbooleanYes—
http_statusinteger | nullYesHTTP status the endpoint returned to the test request.
endpointWebhookEndpointYes—

WebhookOk

object · Required: ok

PropertyTypeRequiredRules
okbooleanYes—

WebhookRetryResult

object · Required: ok, id

PropertyTypeRequiredRules
okbooleanYes—
idstringYes—

WebhookEndpointCreateInput

object · Required: url

PropertyTypeRequiredRules
urlstringYesA public HTTPS endpoint on port 443.
event_typesarray<string>NoWhich events to deliver. Defaults to ["message.sent"].

WebhookEndpointUpdateInput

object

PropertyTypeRequiredRules
urlstringNo—
event_typesarray<string>No—
enabledbooleanNoOnly false is accepted, to disable delivery.

InboxSession

object · Required: url, workspace, base_url, token, expires_at, access

PropertyTypeRequiredRules
urlstringYesOrigin of the inbox service for this environment.
workspacestringYesYour workspace id, the same one the token is bound to.
base_urlstringYesPrefix for every inbox endpoint: {url}/w/{workspace}/api.
tokenstringYesBearer token for the inbox service. Valid for one hour; never store it longer than that.
expires_atstringYes—
accessstringYesAllowed: read, write · write when the key holds inbox:write, otherwise read.

MetricsCampaignPace

object · Required: campaign_id, target_leads, remaining_leads, contacts_last_7_days, send_days_last_7_days

PropertyTypeRequiredRules
campaign_idstringYes—
target_leadsintegerYesLeads on the campaign's target lists.
remaining_leadsintegerYesLeads the campaign has not contacted yet.
contacts_last_7_daysintegerYesLeads first contacted in the last seven days.
send_days_last_7_daysintegerYesHow many of the last seven days were send days for this campaign (its send-day schedule, from the day it was created).