Home
API Docs
Dashboard

API Reference

Connect any app to Sendinel in minutes. Identify contacts, fire events, send transactional email, and wire up AI agents — all through a single REST API or the TypeScript SDK.

Quick Start

Sendinel is an email operations control plane. Send transactional email, run campaigns, track events, and manage contacts — all through a REST API or the MCP server.

No DNS required to start. Every Sendinel account ships with access to mail.sendinel.ai — a shared sending domain that's already configured, warmed, and ready to send. You can add your own domain any time, but you don't need one to get your first email out the door.

Get your API key

Go to Dashboard → Settings → API Keys and create a new key. Keys are prefixed with snk_ and scoped to read, write, or admin. Most integrations need write scope.

Keys are hashed (SHA-256) before storage. The plaintext is shown once and cannot be retrieved again.

Option A — TypeScript SDK (recommended)

bash
npm install @sendinel/sdk
TypeScript
import { Sendinel } from "@sendinel/sdk";

const sendinel = new Sendinel({ apiKey: "snk_..." });

// Identify a user — creates or updates their contact profile
await sendinel.identify("user@example.com", {
  firstName: "Alex",
  properties: { plan: "pro" },
  siteId: "your-site-uuid",   // subscribes them + triggers signup campaigns
});

// Fire a named event — triggers any matching campaign automations
await sendinel.track("user@example.com", "completed_onboarding");

Option B — curl

curl
curl -X POST https://sendinel.ai/api/v1/identify \
  -H "Authorization: Bearer snk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "first_name": "Alex",
    "properties": { "plan": "pro" },
    "site_id": "your-site-uuid"
  }'

TypeScript SDK

@sendinel/sdk is a zero-dependency TypeScript client for the Sendinel v1 API. Works in Node.js 18+ (uses native fetch). Full type coverage included.

bash
npm install @sendinel/sdk

Full example

TypeScript
import { Sendinel, SendinelError } from "@sendinel/sdk";

const sendinel = new Sendinel({
  apiKey: process.env.SENDINEL_API_KEY!,
  // baseUrl: "https://sendinel.ai"  // default, override for self-hosted
});

// Identify — creates or updates a contact profile (idempotent by email)
const { contact_id, created } = await sendinel.identify("alex@example.com", {
  firstName: "Alex",
  lastName: "Smith",
  properties: { plan: "pro", company: "Acme" },
  tags: ["paying", "enterprise"],
  siteId: "your-site-uuid",  // subscribes to site + enrolls in signup campaigns
});

// Track — fires a named event, auto-creates contact if needed
const result = await sendinel.track("alex@example.com", "completed_onboarding", {
  steps_completed: 5,
  duration_seconds: 142,
});
// result.enrolled_campaigns  →  ["Welcome to Pro"]
// result.contact_created     →  false (contact already existed)

// Send — transactional email, one-off
await sendinel.send({
  to: "alex@example.com",
  siteId: "your-site-uuid",
  subject: "Your order is confirmed",
  html: "<p>Order #123 ships tomorrow.</p>",
  idempotencyKey: "order-123",  // prevents duplicate sends
});

Error handling

TypeScript
import { Sendinel, SendinelError } from "@sendinel/sdk";

try {
  await sendinel.identify("bad-email");
} catch (err) {
  if (err instanceof SendinelError) {
    console.log(err.status);  // 400
    console.log(err.message); // "Invalid email address"
    console.log(err.body);    // full JSON response
  }
}

Retry behavior

The SDK retries automatically on network errors, 429 Too Many Requests, and 5xx responses. Up to 3 attempts with exponential backoff (100ms, 200ms, 400ms). Respects Retry-After headers. 4xx errors (except 429) are not retried — they indicate a problem with the request.

Identify API

POST /api/v1/identify

Create or update a contact profile. Safe to call on every user action — idempotent by email address. Authenticated via API key with write scope.

This is the standard integration point. Call identify when a user signs up, logs in, or upgrades — same pattern as Segment and Customer.io. Sendinel merges properties, appends tags, and handles deduplication automatically.

Request body

application/json
{
  "email": "user@example.com",       // required
  "first_name": "Alex",              // optional
  "last_name": "Smith",              // optional
  "properties": {                    // optional — merged (patch semantics, never overwrites)
    "plan": "pro",
    "company": "Acme"
  },
  "tags": ["paying", "enterprise"],  // optional — appended, no duplicates
  "site_id": "uuid"                  // optional — subscribes to site + triggers signup campaigns
}

Response — 201 Created (new contact)

201 Created
{
  "contact_id": "uuid",
  "created": true
}

Response — 200 OK (existing contact updated)

200 OK
{
  "contact_id": "uuid",
  "created": false
}

Merge semantics

FieldBehavior on update
emailPrimary key — not updatable
first_name / last_nameOverwritten only if non-empty string provided
propertiesDeep merge — new keys added, existing keys updated only if re-specified
tagsAppended — existing tags preserved, duplicates removed
site_idUpserts site subscription, enrolls in signup campaigns (first create only)

Examples

curl
curl -X POST https://sendinel.ai/api/v1/identify \
  -H "Authorization: Bearer snk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alex@acme.com",
    "first_name": "Alex",
    "properties": { "plan": "pro", "source": "stripe" },
    "tags": ["paying"],
    "site_id": "your-site-uuid"
  }'
TypeScript (SDK)
const { contact_id, created } = await sendinel.identify("alex@acme.com", {
  firstName: "Alex",
  properties: { plan: "pro", source: "stripe" },
  tags: ["paying"],
  siteId: "your-site-uuid",
});

Events API

POST /api/v1/events

Fire a named event for a contact. Enrolls them in any active triggered campaigns that match the event name. Authenticated via API key with write scope.

Auto-identify on track. If the contact doesn't exist yet, Sendinel creates a minimal profile automatically (email only, source: "api") so the event can be recorded and campaigns can enroll. You don't need to call identify first — though calling both gives you richer contact data.

Request body

application/json
{
  "email": "user@example.com",          // required
  "event": "completed_onboarding",      // required — must match a campaign trigger_event_name
  "data": { "steps_completed": 5 }      // optional — stored in event metadata
}

Success response

200 OK
{
  "event": "completed_onboarding",
  "contact_id": "uuid",
  "contact_created": false,              // true if contact was auto-created
  "enrollments_created": 1,
  "enrolled_campaigns": ["Onboarding Sequence"],
  "skipped_campaigns": []               // already enrolled in these
}

Examples

curl
curl -X POST https://sendinel.ai/api/v1/events \
  -H "Authorization: Bearer snk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alex@acme.com",
    "event": "completed_onboarding",
    "data": { "steps_completed": 5 }
  }'
TypeScript (SDK)
const result = await sendinel.track("alex@acme.com", "completed_onboarding", {
  steps_completed: 5,
});

console.log(result.enrolled_campaigns); // ["Onboarding Sequence"]
console.log(result.contact_created);    // false

How campaign triggering works

When an event is received, Sendinel finds all active campaigns with type: "triggered"and a matching trigger_event_name. For each match, a new enrollment is created if the contact is not already enrolled (any non-exited status). The contact begins receiving the campaign sequence starting from the first step.

If no campaigns match the event name, a 200 is returned with enrollments_created: 0. You can fire test events from the dashboard: open a triggered campaign and click Send Test Event.

Transactional Email API

POST /api/v1/send

Send a single transactional email immediately. Checks plan limits, rate limits, and the suppression list before sending. The from address and name come from the site configuration. Authenticated via API key with write scope.

Request body

application/json
{
  "to": "user@example.com",             // required — recipient email
  "site_id": "uuid",                     // required — identifies sender domain + from address
  "subject": "Order confirmed",          // required — max 998 characters
  "html": "<p>Your order #123...</p>",   // required — HTML body
  "text": "Your order #123...",          // optional — plain text fallback
  "reply_to": "support@company.com",    // optional
  "headers": { "X-Custom": "value" },   // optional — custom headers
  "tags": ["transactional", "order"],   // optional — for filtering in email log
  "idempotency_key": "order-123",        // optional — prevents duplicate sends
  "tracking": true                       // optional — default true (open + click tracking)
}

Success response

200 OK
{
  "id": "uuid",                          // Sendinel email log ID
  "provider_message_id": "re_xxxx",     // Resend (or provider) message ID
  "to": "user@example.com",
  "status": "sent"
}

Examples

curl
curl -X POST https://sendinel.ai/api/v1/send \
  -H "Authorization: Bearer snk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "alex@acme.com",
    "site_id": "your-site-uuid",
    "subject": "Your order is confirmed",
    "html": "<h1>Order #123</h1><p>Ships tomorrow.</p>",
    "idempotency_key": "order-123"
  }'
TypeScript (SDK)
await sendinel.send({
  to: "alex@acme.com",
  siteId: "your-site-uuid",
  subject: "Your order is confirmed",
  html: "<h1>Order #123</h1><p>Ships tomorrow.</p>",
  idempotencyKey: "order-123",
});

Tracking: Open tracking (1x1 pixel) and click tracking (link wrapping) are automatically injected into the HTML body. Set tracking: false to disable.

Conversion Tracking

POST /api/v1/conversion

Track conversions and attribute revenue to email campaigns. Revenue is attributed to the most recent email sent to the contact within a 7-day window. Authenticated via API key with write scope.

Request body

application/json
{
  "email": "user@example.com",       // required
  "event": "purchase",               // optional — default: "purchase"
  "revenue": 49.99,                  // optional
  "currency": "USD",                 // optional — default: "USD"
  "order_id": "order_123",           // optional — deduplication key (409 on duplicate)
  "metadata": { "plan": "pro" }      // optional
}

Success response

200 OK
{
  "id": "uuid",
  "contact_id": "uuid",
  "email_log_id": "uuid",
  "campaign_id": "uuid",
  "event": "purchase",
  "revenue": 49.99,
  "attributed_at": "2026-04-10T12:00:00Z"
}

Attribution logic

Sendinel looks up the most recent email sent to that contact within 7 days. If found, the conversion is attributed to that email and its campaign (last-touch attribution). Revenue data appears in the dashboard overview automatically.

If order_id is provided, duplicate conversions with the same order ID are rejected with 409 Conflict.

Webhooks

Sendinel processes delivery events from your email provider to update status, log opens and clicks, and handle bounces and complaints automatically.

Setup

In your Resend dashboard, add a webhook pointing to:

https://sendinel.ai/api/webhooks/resend

For per-project routing (BYOD with multiple projects):

https://sendinel.ai/api/webhooks/resend/[projectId]

All webhook payloads are verified using Svix signature verification. Set RESEND_WEBHOOK_SECRET in your environment. Per-project secrets are supported and stored encrypted.

Supported events

EventDescriptionAction
email.deliveredEmail accepted by recipient serverUpdates email_log status → 'delivered'
email.openedRecipient opened the emailUpdates status → 'opened', increments open count
email.clickedRecipient clicked a tracked linkUpdates status → 'clicked', logs URL in clicked_urls
email.bouncedHard or soft bounceUpdates status → 'bounced', adds suppression record
email.complainedRecipient marked as spamUpdates status → 'complained', adds suppression, unsubscribes contact

Bounces and complaints trigger Slack alerts if configured. Failed webhook processing is retried via a dead letter queue with exponential backoff (up to 5 attempts).

MCP Integration

Sendinel ships an MCP (Model Context Protocol) server. Every campaign, contact, segment, analytics, and template operation is accessible to AI agents — list campaigns, add contacts, generate emails, check deliverability, approve drafts, and more without writing a line of code.

Two connection methods

TransportBest forHow
stdioLocal clients — Claude Desktop, Cursor, Claude Codenpx @sendinel/mcp-server (env vars injected)
HTTP/SSERemote agents, web apps, non-stdio clientshttps://sendinel.ai/api/mcp/sse — Bearer token auth

Available tool groups

GroupToolsDescription
analytics6Stats, domain health, engagement insights, deliverability checks, performance reports
campaigns10Create, clone, enroll, launch, list enrollments; AI email generation with brand voice
contacts18CRUD, import, merge, tags, segments, preview, test sends, suppressions
segments6Create, update, delete, preview; natural language segment builder
drafts4Create, list, approve, reject AI-generated drafts
templates4Create, list, update, archive email templates
data8Audit log, scoring rules, DMARC reports, contact score explanation, data export
sites2Create and update sending sites
gdpr2Delete contact data (Article 17), list deletion log

API keys can be scoped to specific tool groups. Destructive operations use a two-call confirmation pattern.

AI Clients

Connect Sendinel to any MCP-compatible AI client. Once connected, your agent has all 48 tools available to manage your email operations conversationally.

Claude Desktop

Open Settings → Developer → Edit Config and add:

claude_desktop_config.json — HTTP/SSE (recommended)
{
  "mcpServers": {
    "sendinel": {
      "type": "sse",
      "url": "https://sendinel.ai/api/mcp/sse",
      "headers": {
        "Authorization": "Bearer snk_your_api_key"
      }
    }
  }
}
claude_desktop_config.json — stdio (local / self-hosted)
{
  "mcpServers": {
    "sendinel": {
      "command": "npx",
      "args": ["@sendinel/mcp-server"],
      "env": {
        "SUPABASE_URL": "https://xxx.supabase.co",
        "SUPABASE_SERVICE_ROLE_KEY": "your-service-role-key",
        "SENDINEL_PROJECT_ID": "your-project-uuid"
      }
    }
  }
}

Claude Code

Add to your project's .mcp.json:

.mcp.json
{
  "mcpServers": {
    "sendinel": {
      "type": "sse",
      "url": "https://sendinel.ai/api/mcp/sse",
      "headers": {
        "Authorization": "Bearer snk_your_api_key"
      }
    }
  }
}

Or install the /sendinel Claude Code skill for a guided setup:

bash
npx @sendinel/mcp-setup --install-skill

Cursor

Open Settings → MCP → Add Server and paste:

cursor settings.json — MCP
{
  "mcp": {
    "servers": {
      "sendinel": {
        "url": "https://sendinel.ai/api/mcp/sse",
        "headers": {
          "Authorization": "Bearer snk_your_api_key"
        }
      }
    }
  }
}

OpenAI Codex CLI

~/.codex/config.toml
[[mcp_servers]]
name = "sendinel"
transport = "sse"
url = "https://sendinel.ai/api/mcp/sse"

[mcp_servers.headers]
Authorization = "Bearer snk_your_api_key"

Example agent conversation

MCP tool call
// Your prompt:
"List my active campaigns and tell me which ones have the lowest open rates."

// Agent calls:
list_campaigns({ "status": "active" })
get_stats({ "campaign_id": "uuid", "period": "30d" })

// Returns:
{
  "campaigns": [
    { "name": "Welcome Series", "open_rate": 0.42, "enrolled": 142 },
    { "name": "Re-engagement",  "open_rate": 0.11, "enrolled": 38 }
  ]
}

Rate Limits

Rate limits are enforced per project using a sliding window. Limits vary by plan and operation type.

PlanSend APIRead endpointsWrite endpoints
Free10 req / 5 min60 req / 5 min30 req / 5 min
BYOD100 req / 5 min300 req / 5 min150 req / 5 min
Managed100 req / 5 min300 req / 5 min150 req / 5 min

Rate limit headers

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1744329600

When rate limited, the API returns 429 Too Many Requests with a Retry-After header. The SDK handles 429s automatically with backoff and retry.

Error Codes

All error responses return a JSON body with an error field describing the issue.

StatusMeaningCommon causes
400Bad RequestMissing required fields, invalid email format, body too large
401UnauthorizedMissing or invalid API key, expired key
403ForbiddenAPI key lacks required scope (write scope needed for mutations)
404Not FoundInvalid site_id or resource ID
409ConflictDuplicate idempotency_key (email already sent), duplicate order_id
422UnprocessableContact limit reached — cannot auto-create contact (track endpoint)
429Rate LimitedToo many requests in the current window
500Server ErrorInternal error — retry with exponential backoff
Error response shape
{
  "error": "Descriptive error message",
  "code": "SUPPRESSED_CONTACT",   // optional — machine-readable code
  "details": { ... }              // optional — additional context
}

Need help? support@sendinel.ai

Getting Started