Developer Hub
Connect apps, AI agents, and automation systems to Sendinel in minutes. Start with an API key, wire up REST or MCP, then use llms.txt when an agent needs the compact machine-readable reference.
For AI Agents & LLMs
Point agents at the compact llms.txt reference or the expanded MCP + REST reference before they call Sendinel.
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.
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 → Developer 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)
npm install @sendinel/sdkimport { 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 -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"
}'Copy-paste flows
const SENDINEL_API_KEY = process.env.SENDINEL_API_KEY;
const baseUrl = "https://sendinel.ai";
await fetch(`${baseUrl}/api/v1/identify`, {
method: "POST",
headers: {
Authorization: `Bearer ${SENDINEL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "alex@acme.com",
first_name: "Alex",
properties: { plan: "pro" },
site_id: "your-site-uuid",
}),
});
const eventRes = await fetch(`${baseUrl}/api/v1/events`, {
method: "POST",
headers: {
Authorization: `Bearer ${SENDINEL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "alex@acme.com",
event: "completed_onboarding",
data: { source: "app" },
}),
});
console.log(await eventRes.json());const res = await fetch("https://sendinel.ai/api/v1/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDINEL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "alex@acme.com",
site_id: "your-site-uuid",
subject: "Your order is confirmed",
html: "<h1>Order #123</h1><p>Ships tomorrow.</p>",
text: "Order #123 ships tomorrow.",
tags: ["transactional", "order"],
idempotency_key: "order-123",
tracking: true,
}),
});
if (!res.ok) throw new Error(await res.text());
console.log(await res.json());Machine-Readable References
Sendinel exposes canonical references for humans, SDK generators, and AI agents. Use these instead of scraping this page when you need a complete machine-readable contract.
| Reference | Best for | URL |
|---|---|---|
| REST OpenAPI | SDK generation, endpoint discovery, typed REST clients | /api/v1/openapi.json |
| MCP OpenAPI | Non-MCP agents that call tools over HTTP | /api/mcp/openapi.json |
| Compact LLM reference | Small prompt/context budgets | /llms.txt |
| Full LLM reference | Agents that need payload shapes, safety rules, and examples | /llms-full.txt |
For AI Agents & LLMs
Sendinel publishes llms.txt files so coding agents and LLM tools can discover the API, MCP server, auth model, and safety notes without scraping the whole site. The convention comes from llmstxt.org: expose a concise, stable text reference at the root of the domain for agent consumption.
/llms.txt/llms-full.txtAgent connection path — remote (recommended)
| Step | Action |
|---|---|
| 1 | In Settings → AI, click "Connect to Claude.ai" or copy the MCP URL. |
| 2 | For Claude Code: run the command below. Your browser opens the OAuth approval page. |
| 3 | Approve access — tools appear automatically. No API key or config file needed. |
claude mcp add sendinel --url https://sendinel.ai/mcpAgent connection path — local stdio (advanced)
| Step | Action |
|---|---|
| 1 | Create a project API key in Dashboard → Settings → Developer. |
| 2 | Add @sendinel/mcp-server as a local stdio MCP server and inject SENDINEL_API_KEY. |
| 3 | Use /llms.txt for compact instructions or /llms-full.txt for the complete tool reference. |
{
"mcpServers": {
"sendinel": {
"command": "npx",
"args": ["-y", "@sendinel/mcp-server@latest"],
"env": {
"SENDINEL_API_KEY": "snk_your_api_key"
}
}
}
}REST API Reference
REST endpoints use Bearer token authentication with project-scoped snk_ keys. Send JSON requests to https://sendinel.ai; self-hosted installs can override the base URL.
| Endpoint | Purpose | Details |
|---|---|---|
| POST /api/v1/identify | Create or update contacts | Idempotent by email; can subscribe contacts to a site. |
| POST /api/v1/events | Track named events | Triggers event-based campaign automations. |
| POST /api/v1/send | Send transactional email | Supports idempotency keys to prevent duplicate sends. |
| POST /api/v1/conversion | Track revenue or conversion events | Attributes conversion data back to campaigns and contacts. |
| GET /api/v1/contacts | List contacts | Supports limit, cursor, site_id, tag, and search filters. |
| POST /api/v1/contacts | Create a contact | Supports Idempotency-Key and accepts tags, attribution fields, and site_id. |
| GET /api/v1/sites | List sending sites | Returns configured sender identities for the authenticated project. |
| GET /api/v1/campaigns | List campaigns | Filter by site_id and status; paginated with limit and cursor. |
| POST /api/v1/campaigns | Create campaign shell | Creates a draft campaign for a site; add steps with campaign step endpoints. |
| GET /api/v1/templates | List templates | Filter by category/search; paginated with limit and cursor. |
| POST /api/v1/templates | Create template | Creates a reusable AI/template brief with optional subject hint and HTML. |
| GET /api/v1/analytics/overview | Site analytics summary | Requires site_id; returns delivery, growth, and sender health metrics. |
| GET /api/v1/analytics/campaigns | Campaign analytics | Returns campaign-level performance metrics. |
| GET /api/v1/analytics/conversion | Conversion analytics | Returns attributed conversion and revenue metrics. |
| GET /api/v1/export | Tenant export | Admin-scoped JSON/CSV export for a site. |
Common REST examples
curl "https://sendinel.ai/api/v1/contacts?site_id=site_uuid&limit=50&tag=paying" \
-H "Authorization: Bearer snk_your_api_key"curl -X POST https://sendinel.ai/api/v1/contacts \
-H "Authorization: Bearer snk_your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: contact-alex-acme" \
-d '{
"email": "alex@acme.com",
"first_name": "Alex",
"last_name": "Smith",
"site_id": "site_uuid",
"tags": ["paying", "enterprise"],
"utm_source": "stripe"
}'curl -X POST https://sendinel.ai/api/v1/templates \
-H "Authorization: Bearer snk_your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: template-trial-expiry-v1" \
-d '{
"name": "Trial expiry reminder",
"brief": "Remind the user their trial ends soon and ask them to choose a plan.",
"subject_hint": "Your trial ends in 3 days",
"category": "lifecycle",
"tags": ["trial", "conversion"]
}'curl -X POST https://sendinel.ai/api/v1/campaigns \
-H "Authorization: Bearer snk_your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: campaign-welcome-v1" \
-d '{
"name": "Welcome sequence",
"site_id": "site_uuid",
"type": "drip"
}'curl "https://sendinel.ai/api/v1/analytics/overview?site_id=site_uuid&days=30" \
-H "Authorization: Bearer snk_your_api_key"curl "https://sendinel.ai/api/v1/export?site_id=site_uuid&format=json" \
-H "Authorization: Bearer snk_admin_api_key"SDKs & Integrations
@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.
npm install @sendinel/sdkFull example
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
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.
Connections
Everything Sendinel can connect to — email delivery providers, social platforms, import sources, webhooks, and AI clients.
Email Delivery Providers
Sendinel routes email through your provider of choice. Configure at Settings → Project → Email Provider. Credentials are stored AES-256-GCM encrypted.
Social Publishing Platforms
Sendinel publishes to 12 social platforms through the SocialPublisher abstraction (Upload Post by default). Connect accounts at Settings → Channels → Social → Connect Account. OAuth flow: GET /api/integrations/social/{{platform}}/connect.
Import Sources
Migrate contacts, lists, tags, templates, campaigns, and suppressions into Sendinel from your existing platform. Trigger at Settings → Import Contacts.
Outbound Webhooks
Sendinel fires signed HTTP POST webhooks to your endpoint on key events. Configure at Settings → Developer → Webhooks. Signature: X-Sendinel-Signature (HMAC-SHA256).
campaign.completedemail.bouncedemail.complainedcontact.unsubscribedbounce_rate.warningdomain.dns_lostAI Clients & MCP
Sendinel's 65-tool MCP server can be connected from any MCP-compatible AI client. Known integrations: Synchronex (primary orchestration), Claude Desktop, Cursor.
{
"mcpServers": {
"sendinel": {
"command": "npx",
"args": ["-y", "@sendinel/mcp-server@latest"],
"env": {
"SENDINEL_API_KEY": "snk_...",
"SENDINEL_PROJECT_ID": "uuid"
}
}
}
}Identify API
Create or update a contact profile. Safe to call on every user action — idempotent by email address. Authenticated via API key with write scope.
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
{
"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)
{
"contact_id": "uuid",
"created": true
}Response — 200 OK (existing contact updated)
{
"contact_id": "uuid",
"created": false
}Merge semantics
| Field | Behavior on update |
|---|---|
| Primary key — not updatable | |
| first_name / last_name | Overwritten only if non-empty string provided |
| properties | Deep merge — new keys added, existing keys updated only if re-specified |
| tags | Appended — existing tags preserved, duplicates removed |
| site_id | Upserts site subscription, enrolls in signup campaigns (first create only) |
Examples
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"
}'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
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.
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
{
"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
{
"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 -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 }
}'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); // falseHow 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
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
{
"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
{
"id": "uuid", // Sendinel email log ID
"provider_message_id": "re_xxxx", // Resend (or provider) message ID
"to": "user@example.com",
"status": "sent"
}Examples
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"
}'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
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
{
"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
{
"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 attributes revenue to the most recent email click within 72 hours, falling back to the most recent open within 72 hours. 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/resendFor 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
| Event | Description | Action |
|---|---|---|
| email.delivered | Email accepted by recipient server | Updates email_log status → 'delivered' |
| email.opened | Recipient opened the email | Updates status → 'opened', increments open count |
| email.clicked | Recipient clicked a tracked link | Updates status → 'clicked', logs URL in clicked_urls |
| email.bounced | Hard or soft bounce | Updates status → 'bounced', adds suppression record |
| email.complained | Recipient marked as spam | Updates 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 Tools Reference
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.
Three connection methods
| Transport | Best for | How |
|---|---|---|
| Remote HTTP + OAuth | Claude.ai, Claude Code, Cursor, Windsurf — zero config | claude mcp add sendinel --url https://sendinel.ai/mcp — browser OAuth flow, no API key needed |
| stdio | Local clients with manual config — Claude Desktop | npx @sendinel/mcp-server — inject SENDINEL_API_KEY env var |
| REST/OpenAPI | Non-MCP agents, web apps, custom integrations | /api/mcp/tools + /api/mcp/openapi.json — Bearer API key auth |
OAuth discovery
| Endpoint | Purpose |
|---|---|
| /.well-known/oauth-authorization-server | RFC 8414 discovery — MCP clients find all OAuth endpoints automatically |
| /.well-known/oauth-protected-resource | Points clients to the authorization server |
| /oauth/authorize | Browser approval page — shows client name, project, and requested scopes |
| /oauth/token | Exchange auth code for access + refresh tokens (90 day / 1 year) |
| /oauth/register | Dynamic client registration (RFC 7591) — any MCP client can self-register |
| /oauth/revoke | Revoke a token immediately |
Available tool groups
| Group | Tools | Description |
|---|---|---|
| analytics | 16 | Stats, domain health, engagement insights, deliverability checks, performance reports |
| campaigns | 25 | Create, clone, enroll, launch, generate email, validate content, test sends |
| contacts | 24 | CRUD, import, merge, tags, segments, suppressions |
| segments | 6 | Create, update, delete, preview; natural language segment builder |
| drafts | 6 | Create, list, approve, reject AI-generated drafts |
| sites | 5 | Create and update sending sites |
| gdpr | 2 | Delete contact data (Article 17), list deletion log |
API keys can be scoped to specific tool groups. Destructive operations use a two-call confirmation pattern.
Tool catalog
| Tool | Group | Required arguments |
|---|---|---|
| abuse_monitor_override | analytics | siteId, reason, ttlHours |
| abuse_monitor_status | analytics | siteId |
| create_social_campaign | analytics | name, platform, destination_url |
| deliverability_check | analytics | site_id |
| get_domain_health | analytics | site_id |
| get_engagement_insights | analytics | - |
| get_portfolio_analytics | analytics | - |
| get_portfolio_stats | analytics | - |
| get_site_insights | analytics | site_id |
| get_sites | analytics | - |
| get_stats | analytics | site_id, start_date |
| list_event_log | analytics | - |
| list_projects | analytics | - |
| list_social_campaigns | analytics | - |
| performance_report | analytics | - |
| schedule_social_post | analytics | campaign_id, content, publisher |
| add_campaign_step | campaigns | campaign_id, step_order, subject |
| clone_campaign | campaigns | campaign_id, new_name |
| create_campaign | campaigns | site_id, name, type |
| create_campaign_with_content | campaigns | site_id, name, type, steps |
| delete_campaign | campaigns | campaign_id |
| delete_campaign_step | campaigns | step_id |
| enroll_contact | campaigns | campaign_id, contact_id |
| enroll_segment | campaigns | campaign_id, site_id |
| generate_email | campaigns | site_id, brief |
| get_campaign | campaigns | campaign_id |
| get_campaign_calendar | campaigns | year, month |
| get_enrollment_status | campaigns | campaign_id, contact_id |
| get_schedule | campaigns | site_id |
| launch_campaign | campaigns | name, site_id, emails |
| list_campaign_steps | campaigns | campaign_id |
| list_campaigns | campaigns | - |
| preview_campaign_audience | campaigns | campaign_id |
| preview_email | campaigns | - |
| send_test_email | campaigns | to_email, site_id |
| send_transactional | campaigns | to, site_id, subject |
| unenroll_contact | campaigns | campaign_id, contact_id |
| update_campaign | campaigns | campaign_id |
| update_campaign_status | campaigns | campaign_id, status |
| update_campaign_step | campaigns | step_id |
| validate_template | campaigns | html |
| add_subscriber | contacts | email, site_id |
| add_suppression | contacts | |
| estimate_segment_size | contacts | - |
| find_duplicates | contacts | - |
| get_contact_schema | contacts | site_id |
| get_hygiene_config | contacts | - |
| get_subscriber | contacts | - |
| get_subscriber_timeline | contacts | subscriber_id |
| identify_inactive | contacts | - |
| import_subscribers | contacts | contacts, site_id |
| list_email_log | contacts | - |
| list_hygiene_audit | contacts | siteId |
| list_subscribers | contacts | - |
| list_subscribers_by_segment | contacts | - |
| list_suppressions | contacts | - |
| list_win_back_draft | contacts | siteId |
| merge_subscribers | contacts | target_id, source_ids |
| remove_suppression | contacts | |
| run_list_hygiene | contacts | - |
| set_subscriber_tags | contacts | contact_ids |
| subscriber_export | contacts | contactId |
| unsubscribe_subscriber | contacts | |
| update_hygiene_config | contacts | - |
| update_subscriber | contacts | contact_id |
| create_segment | segments | name, rules |
| create_segment_nl | segments | name, description |
| delete_segment | segments | segment_id |
| list_segments | segments | - |
| preview_segment | segments | - |
| update_segment | segments | segment_id |
| approve_draft | drafts | draft_id |
| create_draft | drafts | site_id, subject, body_html |
| get_draft | drafts | draft_id |
| list_drafts | drafts | - |
| promote_draft_to_campaign | drafts | draft_id |
| reject_draft | drafts | draft_id |
| create_sender | sites | name, slug, from_name |
| create_site | sites | name, slug, from_name |
| get_brand_voice | sites | site_id |
| update_brand_voice | sites | site_id, brand_voice |
| update_site | sites | site_id |
| delete_subscriber_data | gdpr | contact_id |
| list_deletion_log | gdpr | - |
| check_ab_significance | ab-testing | test_id |
| create_ab_test | ab-testing | campaign_id, name |
| list_ab_tests | ab-testing | - |
| promote_ab_winner | ab-testing | test_id |
| campaign_advisor | advisor | - |
| optimize_subject_lines | advisor | campaign_id, site_id |
| recovery_plan | advisor | site_id |
| run_portfolio_analysis | advisor | - |
| suggest_content_topics | advisor | site_id |
| suggest_send_time | advisor | site_id |
| delete_webhook_subscription | webhooks | subscription_id |
| list_webhook_subscriptions | webhooks | - |
| subscribe_webhook | webhooks | url, events |
| update_webhook_subscription | webhooks | subscription_id |
| check_approval | approvals | approval_id |
| list_pending_approvals | approvals | - |
| enroll_automation | automations | subscriber_id |
| list_automations | automations | - |
| preview_automation | automations | source_id |
| trigger_automation | automations | source_id |
| create_template | templates | name, brief |
| delete_template | templates | id |
| get_template | templates | id |
| list_templates | templates | - |
| translate_template | templates | - |
| update_template | templates | id |
| dnsbl_check | delivery-ops | siteId |
| dnsbl_delisting_draft | delivery-ops | siteId, zone |
| explain_contact_score | delivery-ops | - |
| export_data | delivery-ops | resource |
| get_cron_runs | delivery-ops | - |
| get_queue_status | delivery-ops | - |
| get_scoring_rules | delivery-ops | - |
| get_send_dlq | delivery-ops | - |
| list_domains | delivery-ops | - |
| refresh_domain_dns | delivery-ops | domain_id |
| register_domain | delivery-ops | site_id, domain |
| update_scoring_rules | delivery-ops | - |
| create_template_from_brief | compound | site_id, name, brief |
| diagnose_delivery_issue | compound | - |
| onboard_new_site | compound | name, domain |
| setup_campaign_from_brief | compound | site_id, campaign_name, brief |
| configure_platform_trigger | data | platform, event_type, action |
| connect_platform | data | platform, credentials |
| create_api_key | data | name |
| disconnect_platform | data | - |
| list_api_keys | data | - |
| list_connected_platforms | data | - |
| list_import_history | data | - |
| list_notifications | data | - |
| list_platform_events | data | - |
| mark_notifications_read | data | - |
| migrate_campaigns_from | data | provider, oauth_token |
| revoke_api_key | data | key_id |
| start_platform_migration | data | connection_id |
| tenant_export_full | data | siteId |
| create_content_block | content | name, html |
| delete_content_block | content | block_id |
| list_content_blocks | content | - |
| list_content_sources | content | - |
| trigger_content_pipeline | content | - |
| get_warmup_status | warmup | site_id |
| pause_warmup | warmup | site_id |
| resume_warmup | warmup | site_id |
| update_warmup_schedule | warmup | site_id |
| create_form | forms | site_id, name, fields |
| get_form_stats | forms | form_id |
| list_forms | forms | - |
| get_plan_limits | org | - |
| get_plan_usage | org | - |
| invite_team_member | org | |
| list_team_members | org | - |
| get_sms_provider_status | sms | - |
| list_sms_inbound | sms | - |
| list_sms_log | sms | - |
| preview_sms_cost | sms | body |
| send_sms | sms | body |
| send_sms_campaign | sms | site_id, name, body |
| set_contact_sms_consent | sms | contact_id, status |
| adapt_social_post | social-posts | email_campaign_id, platforms |
| create_social_post | social-posts | title, content, platforms |
| delete_social_post | social-posts | post_id |
| get_social_post | social-posts | post_id |
| list_social_posts | social-posts | - |
| update_social_post | social-posts | post_id |
AI Clients
Connect Sendinel to any MCP-compatible AI client. Once connected, your agent can manage your email operations conversationally through the MCP tool catalog.
Open Settings -> Developer -> Edit Config, then add this local MCP server. Use this when you normally work in Git Bash but the desktop AI client is launched by Windows.
{
"mcpServers": {
"sendinel": {
"command": "C:\\Program Files\\Git\\bin\\bash.exe",
"args": [
"-lc",
"npx -y @sendinel/mcp-server@latest"
],
"env": {
"SENDINEL_API_KEY": "snk_your_api_key"
}
}
}
}Example agent conversation
// 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 }
]
}Delivery Semantics
Email delivery is asynchronous infrastructure, not a synchronous HTTP side effect. Sendinel accepts API requests, applies safety policy, queues work, and then workers send through the configured provider. This section explains what the API response means and why a message may wait before leaving the queue.
| State | Meaning | Common reason |
|---|---|---|
| accepted | Sendinel validated the request and created queue/log records | Normal response from API or scheduler |
| queued | Message is waiting for an eligible worker slot | Batch limits, warmup cap, send window, blackout date |
| sent | Provider accepted the message for delivery | Resend/Mailgun/SES accepted the API call |
| delivered | Recipient server accepted the message | Provider webhook received |
| opened / clicked | Recipient engagement was tracked | Tracking pixel or wrapped link fired |
| bounced / complained | Recipient server rejected or user reported spam | Contact is suppressed automatically |
What “sent” means
sent means the sending provider accepted the message from Sendinel. It does not mean the recipient inbox accepted it. Final delivery, bounce, complaint, open, and click states arrive later through provider webhooks.
Why sends wait in queue
| Control | Effect |
|---|---|
| Warmup caps | New or recently migrated domains may be limited to a small daily volume until trust is established. |
| Provider warmup | If the email provider also reports a dedicated-IP warmup cap, Sendinel uses the lower of both limits. |
| Plan batch size | Worker batch sizes scale by plan tier; larger plans drain queues faster. |
| Send windows | Scheduled sends only fire inside the configured allowed days and times. |
| Blackout dates | Campaign sends pause on configured blackout days. |
| Suppression checks | Bounced, complained, unsubscribed, or manually suppressed contacts are skipped before provider send. |
| Idempotency | Duplicate transactional requests with the same idempotency key return the existing result instead of sending twice. |
Rate Limits & Plans
Rate limits are enforced per project using a sliding window. Limits vary by plan and operation type.
| Plan | Send API | Read endpoints | Write endpoints |
|---|---|---|---|
| Free | 10 req / 5 min | 60 req / 5 min | 30 req / 5 min |
| BYOD | 100 req / 5 min | 300 req / 5 min | 150 req / 5 min |
| Managed | 500 req / 5 min | 600 req / 5 min | 200 req / 5 min |
| Agency Plus | 1000 req / 5 min | 1200 req / 5 min | 400 req / 5 min |
| Enterprise | 1000 req / 5 min | 1200 req / 5 min | 400 req / 5 min |
Rate limit headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1744329600When 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.
| Status | Meaning | Common causes |
|---|---|---|
| 400 | Bad Request | Missing required fields, invalid email format, body too large |
| 401 | Unauthorized | Missing or invalid API key, expired key |
| 403 | Forbidden | API key lacks required scope (write scope needed for mutations) |
| 404 | Not Found | Invalid site_id or resource ID |
| 409 | Conflict | Duplicate idempotency_key (email already sent), duplicate order_id |
| 422 | Unprocessable | Contact limit reached — cannot auto-create contact (track endpoint) |
| 429 | Rate Limited | Too many requests in the current window |
| 500 | Server Error | Internal error — retry with exponential backoff |
{
"error": "Descriptive error message",
"code": "SUPPRESSED_CONTACT", // optional — machine-readable code
"details": { ... } // optional — additional context
}Need help? support@sendinel.ai