# Sendinel - Full LLM Reference Version: 1.1 | Updated: 2026-06 Base URL: https://sendinel.ai --- ## What is Sendinel? Sendinel is an email + social operations control plane. It is the orchestration layer between AI agents and multi-channel delivery infrastructure. Use it to: - Send transactional and marketing emails through Resend, SendGrid, SES, Postmark, or Mailgun - Publish social posts to 12 platforms (Instagram, LinkedIn, X, Facebook, TikTok, YouTube, Pinterest, Reddit, Threads, Bluesky, Google Business) via the SocialPublisher abstraction - Manage contacts, segments, campaigns, templates, sequences, and social posts in one unified planner - Import contacts, campaigns, and templates from Customer.io, Mailchimp, Klaviyo, ActiveCampaign, Kit, or Resend - Analyze deliverability, engagement, DMARC reports, and cross-channel attribution - Control everything from an AI agent via the 65-tool MCP server or the REST API Sendinel is NOT a sending provider — it routes through your provider of choice. Think of it as Customer.io + Metricool, but built to be controlled by AI. --- ## Authentication Every REST request requires: Authorization: Bearer snk_ - Keys are project-scoped (one project = one sending site/brand) - Key format: snk_ followed by 64 hex characters - Create keys at: Settings -> Developer -> API Keys - Scopes: read (GET only), write (GET + mutations), admin (write + destructive ops) - Granular scopes: contacts:read, contacts:write, campaigns:read, campaigns:write, templates:read, templates:write, segments:read, segments:write, sites:read, sites:write --- ## Standard Error Envelope All errors return: { "error": { "code": "ERROR_CODE", "message": "Human-readable message", "details": {} } } Common codes: BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, UNPROCESSABLE_ENTITY, RATE_LIMITED, INTERNAL_ERROR, PLAN_LIMIT, MISSING_FIELD, INSUFFICIENT_SCOPE --- ## Idempotency POST and PATCH requests accept: Idempotency-Key: Same key + same path = cached response for 24 hours. Use to safely retry on network failures. --- ## Pagination Collection endpoints support cursor-based pagination: GET /api/v1/contacts?limit=50&cursor= Response includes next_cursor (null when exhausted). --- ## REST API Reference ### Transactional (no scope required - write scope on the key suffices) POST /api/v1/send Send a transactional email immediately. Body: { to: string, subject: string, html?: string, text?: string, from?: string, reply_to?: string, template_id?: string, variables?: object } Returns: { id: string, status: "sent" | "queued" } POST /api/v1/identify Upsert a contact by email. Body: { email: string, first_name?: string, last_name?: string, properties?: object } Returns: { contact: Contact, created: boolean } POST /api/v1/trigger Trigger a campaign enrollment by event name. Body: { event: string, email: string, properties?: object } Returns: { enrolled: boolean, campaign_id?: string } POST /api/v1/events Record a contact event. Body: { email: string, event: string, properties?: object, timestamp?: string } Returns: { ok: true } POST /api/v1/conversion Record a conversion event tied to an email. Body: { tracking_token: string, revenue?: number, currency?: string } Returns: { ok: true } ### Contacts (scope: contacts:read / contacts:write) GET /api/v1/contacts Query params: cursor, limit (max 200), site_id, tag, search Returns: { contacts: Contact[], next_cursor: string | null } POST /api/v1/contacts Body: { email: string (required), first_name?, last_name?, tags?: string[], site_id? } Returns: Contact (201) GET /api/v1/contacts/:id Returns: Contact PATCH /api/v1/contacts/:id Body: any Contact fields (all optional) Returns: Contact DELETE /api/v1/contacts/:id Unsubscribes and removes the contact. Returns: 204 Contact shape: { id: uuid, email: string, first_name: string|null, last_name: string|null, tags: string[], subscribed: boolean, score: number, lifecycle_stage: string|null, created_at: datetime } ### Campaigns (scope: campaigns:read / campaigns:write) GET /api/v1/campaigns Query params: cursor, limit, site_id, status (draft|active|paused|completed|archived) Returns: { campaigns: Campaign[], next_cursor: string | null } POST /api/v1/campaigns Body: { name: string (required), site_id: uuid (required), type?: "drip"|"broadcast", description? } Returns: Campaign (201) GET /api/v1/campaigns/:id Returns: Campaign PATCH /api/v1/campaigns/:id Body: { status?: string, name?: string } Returns: Campaign GET /api/v1/campaigns/:id/steps Returns: { steps: CampaignStep[] } POST /api/v1/campaigns/:id/steps Body: { subject: string (required), body_html?, body_text?, delay_days?, delay_hours?, step_order? } Returns: CampaignStep (201) GET /api/v1/campaigns/:id/steps/:stepId Returns: CampaignStep PATCH /api/v1/campaigns/:id/steps/:stepId Body: any step fields (all optional) Returns: CampaignStep DELETE /api/v1/campaigns/:id/steps/:stepId Only works on draft campaigns. Returns: 204 Campaign shape: { id: uuid, name: string, status: string, type: string, site_id: uuid, segment_id: uuid|null, send_at: datetime|null, created_at: datetime } CampaignStep shape: { id: uuid, campaign_id: uuid, step_order: integer, subject: string, body_html: string|null, body_text: string|null, delay_days: integer|null, delay_hours: integer|null } ### Templates (scope: templates:read / templates:write) GET /api/v1/templates Query params: cursor, limit, category, search Returns both project-owned templates and system templates (project_id: null). Returns: { templates: Template[], next_cursor: string | null } POST /api/v1/templates Body: { name: string (required), brief: string (required), subject_hint?, body_html?, category?, description?, tags?: string[] } "brief" is the AI content brief - what this template should accomplish. Returns: Template (201) GET /api/v1/templates/:id Returns: Template PATCH /api/v1/templates/:id Body: any template fields (all optional). System templates (project_id: null) return 403. Returns: Template DELETE /api/v1/templates/:id Soft-deletes (sets is_archived=true). System templates return 403. Returns: 204 Template shape: { id: uuid, name: string, category: string, description: string, subject_hint: string, brief_template: string, body_html: string|null, tags: string[], project_id: uuid|null, is_archived: boolean, created_at: datetime, updated_at: datetime } ### Segments (scope: segments:read / segments:write) GET /api/v1/segments Query params: cursor, limit, site_id Returns: { segments: Segment[] } POST /api/v1/segments Body: { name: string (required), rules: SegmentRule[] (required), site_id? } Returns: Segment (201) GET /api/v1/segments/:id Returns: Segment PATCH /api/v1/segments/:id Body: { name?, rules? } Returns: Segment DELETE /api/v1/segments/:id Returns: 204 Segment shape: { id: uuid, name: string, rules: object[], estimated_count: integer|null, created_at: datetime } SegmentRule shape: { field: string, operator: "eq"|"neq"|"contains"|"gt"|"lt"|"is_set"|"is_not_set", value: any } ### Sites (scope: sites:read / sites:write) GET /api/v1/sites Returns: { sites: Site[] } POST /api/v1/sites Body: { name: string (required), domain?: string, provider?: "resend"|"sendgrid"|"ses"|"postmark"|"mailgun"|"socketlabs" } Returns: Site (201) GET /api/v1/sites/:id Returns: Site PATCH /api/v1/sites/:id Body: any Site fields (all optional) Returns: Site Site shape: { id: uuid, name: string, domain: string|null, provider: string, is_shared_domain: boolean, created_at: datetime } --- ## MCP Server Reference Connection string for Claude Desktop / claude_desktop_config.json: { "mcpServers": { "sendinel": { "command": "npx", "args": ["-y", "@sendinel/mcp-server@latest"], "env": { "SENDINEL_API_KEY": "snk_...", "SENDINEL_PROJECT_ID": "uuid" } } } } Connection string for HTTP transport (Claude.ai or agent frameworks): MCP endpoint: https://mcp.sendinel.ai Auth: Bearer snk_ ### Tool Reference by Group ANALYTICS get_stats(site_id?, period?) -> email stats (sent, open rate, click rate, bounce rate) get_domain_health(site_id?) -> SPF/DKIM/DMARC status per domain get_engagement_insights(site_id?, days?) -> top performing emails and segments deliverability_check(site_id?) -> deliverability score with recommendations performance_report(site_id?, period?) -> full performance breakdown CAMPAIGNS list_campaigns(site_id?, status?) -> campaign list create_campaign(name, site_id, type?, description?) -> new campaign list_campaign_steps(campaign_id) -> steps in send order add_campaign_step(campaignId, subject, bodyHtml?, bodyText?, delayDays?, delayHours?, stepOrder?) -> new step update_campaign_step(step_id, subject?, body_html?, delay_days?, delay_hours?, step_order?) -> updated step delete_campaign_step(step_id) -> removes step (draft campaigns only) update_campaign_status(id, status) -> draft|active|paused|completed|archived enroll_contact(campaignId, contactId) -> enrolls one contact get_enrollment_status(campaignId, contactId) -> current enrollment status and sequence position enroll_segment(campaignId, segmentId) -> bulk enrolls a segment clone_campaign(id, newName?) -> cloned campaign generate_email(campaignId, stepId, brief?) -> AI-generates email content launch_campaign(id) -> activates campaign for sending CONTACTS list_subscribers(site_id?, limit?, cursor?, tag?, search?) -> paginated contact list add_subscriber(email, firstName?, lastName?, tags?, siteId?) -> new contact import_subscribers(contacts[], siteId?) -> bulk import update_subscriber(id, ...fields) -> update contact get_subscriber(id) -> single contact unsubscribe_subscriber(id, reason?) -> unsubscribes contact set_subscriber_tags(id, tags[]) -> replaces tag set list_email_log(contactId?) -> sent email history list_suppressions(siteId?) -> bounced/unsubscribed list add_suppression(email, reason?) -> manual suppression remove_suppression(email) -> re-enables contact send_test_email(to, subject, bodyHtml, siteId?) -> test send SEGMENTS create_segment(name, rules[], siteId?) -> new segment list_segments(siteId?) -> all segments update_segment(id, name?, rules?) -> updated segment delete_segment(id) -> removes segment preview_segment(id, limit?) -> count + sample contacts create_segment_nl(description) -> AI-creates segment from natural language (two-call pattern) TEMPLATES list_templates(siteId?, category?, search?) -> all templates (owned + system) get_template(id) -> single template create_template(name, briefTemplate, subjectHint?, bodyHtml?, category?, description?, tags?) -> new template update_template(id, name?, briefTemplate?, subjectHint?, bodyHtml?, category?) -> updated template delete_template(id) -> soft-deletes template DRAFTS create_draft(campaignId, stepId, subject, bodyHtml) -> new draft for approval list_drafts(status?) -> pending|approved|rejected drafts approve_draft(id) -> approves for sending reject_draft(id, reason?) -> rejects with feedback SITES create_site(name, domain?, provider?) -> new site update_site(id, ...fields) -> updated site get_sites() -> all sites for project list_domains(siteId?) -> domains with health status register_domain(siteId, domain) -> registers domain, returns DNS records to configure DELIVERY DIAGNOSTICS get_cron_runs(job_name?, limit?, since_hours?) -> recent cron job execution history get_queue_status(siteId?) -> pending/processing/failed counts in send queue get_send_dlq(limit?, exhausted_only?) -> dead-letter queue entries export_data(resource, siteId?, since?, limit?) -> JSON export of contacts|email_log|campaigns|suppressions SCORING get_scoring_rules() -> current scoring weights and thresholds update_scoring_rules(open_weight?, click_weight?, send_weight?, open_cap?, click_cap?, decay_penalty_30d?, ...) -> updated rules explain_contact_score(contact_id) -> score breakdown with event history GDPR delete_subscriber_data(email) -> full GDPR erasure (irreversible) list_deletion_log(limit?) -> erasure audit log AUTOMATIONS list_automations() -> configured automations preview_automation(id) -> shows trigger + enrolled contacts trigger_automation(id, email) -> manually fires automation for a contact COMPOUND (multi-step AI workflows) setup_campaign_from_brief(site_id, campaign_name, brief, num_steps?, step_delay_hours?, campaign_type?) -> Creates template + campaign + N AI-generated steps in one call. Returns campaign_id + step_ids. create_template_from_brief(site_id, name, brief, category?) -> AI-generates polished HTML template from brief + brand voice. Returns template_id. diagnose_delivery_issue(site_id?, symptom?) -> Aggregates cron health + queue + DLQ + domain health, returns AI root-cause analysis. onboard_new_site(name, domain, provider?, daily_send_volume?, brand_settings?) -> Creates site, prepares DNS records, initializes warmup plan. Returns checklist. --- ## Social Publishing Sendinel publishes to social platforms through the SocialPublisher abstraction. The default implementation uses Upload Post, which supports OAuth-based multi-tenant account connections. ### Connect a social account (OAuth) GET /api/integrations/social/{platform}/connect Platform values: instagram | linkedin | twitter | facebook | tiktok | youtube | pinterest | reddit | threads | bluesky | google_business Creates a tenant profile if needed and returns a redirect to the OAuth connect URL. The user authorizes the connection in their browser; the platform account is then available for publishing. ### Publish a social post (via MCP or dashboard) Social posts are created through the dashboard Composer or via direct adapter calls from MCP tools. Posts are stored in email.social_posts and tracked with an upload_post_request_id for status polling. Social post shape: { id: uuid, org_id: uuid, project_id: uuid, platforms: string[], content: string, media_urls: string[], scheduled_at: datetime|null, timezone: string|null, status: "draft"|"scheduled"|"published"|"failed", upload_post_request_id: string|null, email_campaign_id: uuid|null, created_at: datetime } ### Cross-channel campaigns Social posts may be linked to an email campaign via email_campaign_id. The unified planner calendar shows both email sends and social posts on a shared timeline with a channel toggle (Email / Social / All). The "Promote to social" flow adapts existing email campaign copy into per-platform captions via Brand Brain (tone_words, forbidden_phrases enforced per platform character limits). ### Supported platforms and limits | Platform | Video | Photo | Text | Document | Notes | |----------------|-------|-------|------|----------|--------------------------------| | Instagram | ✓ | ✓ | — | — | Reels default for video | | LinkedIn | ✓ | ✓ | ✓ | ✓ | Document = PDF carousel | | Twitter/X | ✓ | ✓ | ✓ | — | Max 4 images; link tax on URLs | | Facebook | ✓ | ✓ | ✓ | — | | | TikTok | ✓ | — | — | — | Video only | | YouTube | ✓ | — | — | — | Video only | | Pinterest | ✓ | ✓ | — | — | Requires board_id | | Reddit | ✓ | ✓ | ✓ | — | Requires title field | | Threads | — | ✓ | ✓ | — | | | Bluesky | — | ✓ | ✓ | — | | | Google Business| — | ✓ | ✓ | — | Requires location_id | --- ## Connected Services ### Email Delivery (Sendinel → Provider) Configure at Settings → Project → Email Provider. Credentials stored AES-256-GCM encrypted. - Resend (default): resend.com — API key - SendGrid: sendgrid.com — API key - Postmark: postmarkapp.com — server token - Amazon SES: AWS region + access key + secret key - Mailgun: mailgun.com — API key + domain ### Social Publishing (Sendinel → Platform via Upload Post) Connect accounts at Settings → Channels → Social → Connect Account. Platforms: Instagram, LinkedIn, Twitter/X, Facebook, TikTok, YouTube, Pinterest, Reddit, Threads, Bluesky, Google Business. OAuth flow initiated at GET /api/integrations/social/{platform}/connect. ### Import Sources (Platform → Sendinel) Migrate contacts, tags, lists, templates, campaigns, and suppressions. Trigger: Settings → Import Contacts → Choose Provider. - Customer.io: API key + site ID - Mailchimp: API key - Klaviyo: private API key - ActiveCampaign: account URL + API key - Kit (formerly ConvertKit): API key - Resend: API key ### Webhooks (Sendinel → Your App) Configure at Settings → Developer → Webhooks. Events: campaign.completed, email.bounced, email.complained, contact.unsubscribed, bounce_rate.warning, domain.dns_lost. Delivery: HTTP POST with HMAC-SHA256 signature in X-Sendinel-Signature header. ### Alerts (Sendinel → BetterStack) P1 webhook: BETTERSTACK_P1_WEBHOOK env var — fires on all-providers-open or complaint_rate > 0.3%. P2 webhook: BETTERSTACK_P2_WEBHOOK env var — fires on DNSBL listing. ### MCP Clients Any MCP-compatible AI client can control Sendinel via the 65-tool MCP server. Known integrations: Synchronex (primary), Claude Desktop, Cursor. HTTP endpoint: https://mcp.sendinel.ai (Bearer snk_ auth). --- ## Common Workflows ### Send a transactional email curl -X POST https://sendinel.ai/api/v1/send \ -H "Authorization: Bearer snk_..." \ -H "Content-Type: application/json" \ -d '{"to":"user@example.com","subject":"Welcome!","html":"

Welcome

"}' ### Create and launch a drip campaign (agent) 1. create_campaign(name="Onboarding", site_id="...", type="drip") 2. add_campaign_step(campaignId, subject="Welcome", bodyHtml="...", delay_hours=0) 3. add_campaign_step(campaignId, subject="Day 3 Check-in", bodyHtml="...", delay_days=3) 4. enroll_segment(campaignId, segmentId) 5. update_campaign_status(id, "active") ### Set up a new brand (agent) onboard_new_site(name="Acme Corp", domain="mail.acme.com", provider="resend", daily_send_volume=500) -> Returns DNS records to configure + verification checklist ### Diagnose why emails aren't sending (agent) diagnose_delivery_issue(symptom="campaigns launched but no sends logged in the last 24 hours") -> Returns root cause analysis and corrective action steps --- ## Rate Limits - Free plan: 100 AI calls/day, 1,000 sends/month - Byod plan: 1,000 AI calls/day, 50,000 sends/month - Managed plan: 5,000 AI calls/day, 250,000 sends/month Rate-limited responses: HTTP 429 with { "error": { "code": "RATE_LIMITED", "message": "..." } }