# iris iris is the ai messenger. it runs multi-channel outreach campaigns: create campaigns with AI-generated personalized content, discover leads on Instagram and TikTok, manage contacts, automate email and social messaging, and track conversations across email, Instagram, and Twitter. website: https://www.iris-ai.dev docs: https://www.iris-ai.dev/docs llms-full.txt: https://api.iris-ai.dev/llms-full.txt openapi: https://api.iris-ai.dev/openapi.json ## what iris is - an AI-powered outreach automation platform for email, Instagram DMs, and Twitter DMs - a lead discovery engine that finds contacts on Instagram and TikTok (plus public business listings for venue campaigns) - a campaign management system with objective-driven workflows (venue booking, product promotion, custom outreach) - a unified inbox that aggregates conversations across all channels with real-time updates - a developer platform with a REST API (80+ endpoints), MCP server, webhooks, and agent payment protocols (x402, MPP) ## what iris is not - not a general-purpose CRM (focused on outreach automation, not deal pipelines) - not an email client or ESP (uses SendGrid for delivery) - not a social media scheduler (sends direct messages, not public posts) ## key features - multi-channel outreach (email, Instagram, Twitter) - AI content generation and personalization via Claude - automated contact discovery on Instagram and TikTok - objective-driven campaigns (venue booking, product promotion, custom) - AI-analyzed target profiles for product promotion campaigns - message approval workflow with AI feedback - unified inbox across all channels with real-time updates - real-time analytics and engagement tracking - developer REST API with scoped API keys and webhooks - MCP server for AI agent integration - agent payments (x402 crypto and MPP Stripe) for autonomous agent workflows; discoverable on agentic.market and the CDP x402 bazaar ## discoverable on - agentic.market: https://agentic.market/?service=www-iris-ai-dev - CDP x402 bazaar: https://api.cdp.coinbase.com/platform/v2/x402/discovery/resources (resource: https://api.iris-ai.dev/campaigns) - /.well-known/x402: https://api.iris-ai.dev/.well-known/x402 ## quick start 1. create a campaign - choose objective type (venue booking, product promotion, or custom outreach) 2. add target profiles - define who you want to reach (AI generates profiles for product promotion) 3. discover contacts - search Instagram and TikTok, or import a CSV 4. review AI drafts - iris generates personalized messages for each contact using Claude 5. activate - approve drafts and iris sends them across your chosen channels ## channels ### email personalized outbound and reply tracking from your verified sender email. iris drafts a one-to-one email for each contact, sends from your verified sender email, and routes every reply back into a unified inbox alongside Instagram and Twitter conversations. **features:** - AI-drafted per contact: every email starts from the contact's profile and the campaign brief. no merge tokens, no template blast. - reply, open, and click tracking: iris records delivery, opens, clicks, replies, bounces, and unsubscribes so you can see who engaged and who didn't. - inbound reply threading: replies attach to the originating conversation automatically, so the inbox stays threaded across long exchanges. - approval workflow: every drafted message lands in a review queue. approve as-is, edit inline, or regenerate before anything leaves your inbox. - unsubscribe-compliant: a per-contact unsubscribe link is embedded in every send. one click stops all future outreach automatically. **tips:** - keep subject lines under 50 characters: short subjects survive mobile preview clipping - personalize beyond the name field. referencing recent activity or specific work outperforms generic openers - space out sends rather than queueing them all at once. randomized delays between messages keep the cadence from looking automated - cap follow-ups so you don't burn the relationship. two well-spaced touches beat five rapid-fire ones ### Instagram DM outreach with reply threading and conservative pacing. iris connects to your Instagram account and sends DMs that match the platform's tone: short, conversational, lowercase. delivery and read receipts come back automatically, and replies land in the same inbox as your email and Twitter conversations. **features:** - AI-drafted DMs in Instagram voice: the same campaign brief gets a different treatment for Instagram: shorter, more conversational, no formal openers. - delivery and read receipts: iris flags when a DM is delivered and seen, so you know if a quiet thread was ignored or just unread. - conservative pacing: sends are paced well below platform thresholds and spread across the day to keep your account safe. - reply threading by handle: inbound DMs match the contact by Instagram handle and append to the existing conversation rather than starting a new thread. - secure account connection: connection tokens are encrypted at rest and refreshed automatically. revoke access from your Instagram settings at any time. **tips:** - keep the first DM short. long openers read as automated - open with a specific reference to recent posts rather than a generic compliment - don't send to accounts that have never followed you back if you want to avoid the message-request folder - give 24+ hours between follow-ups. same-day reminders look spammy on Instagram ### Twitter DMs to founders, creators, and prospects with full conversation history. iris uses your Twitter account to send direct messages with full conversation context. inbound DMs get matched to the campaign contact and surface in the same inbox as your other channels. **features:** - DM-first outreach: iris reaches out via DM rather than public reply, keeping the conversation private and out of the algorithmic feed. - longer messages when context calls for it: Twitter DMs aren't capped at 280 characters. iris uses more room when needed but keeps drafts tight by default. - near-real-time reply ingestion: inbound DMs trigger updates in your inbox quickly, so the back-and-forth feels native to Twitter. - automatic session refresh: iris keeps your connection alive in the background, so you don't get logged out mid-campaign. - rate-limit aware: sends are paced conservatively and respect platform limits to keep your account in good standing. **tips:** - lead with the value, not the ask. Twitter DMs that open with 'quick question' get muted - reference a specific tweet to prove you actually read their feed - keep DMs short even when the platform allows longer. short messages get replies - the message-request inbox catches DMs from accounts you don't follow. start a real conversation before pitching ## how-to guides ### getting started with iris create an account, verify a sender email, and run your first campaign in under ten minutes. difficulty: beginner | estimated time: 10 minutes **steps:** 1. **create your account** - sign up at iris-ai.dev with email or Google. the free tier includes one campaign per month so you can run a real outreach end-to-end before deciding on a plan. 2. **verify a sender email** - if you plan to send email, head to settings → channels and add the address you want emails to come from (e.g. you@yourcompany.com). SendGrid sends a verification link to that inbox; click it to confirm ownership. follow the verify-sender-email guide for a non-technical walkthrough. tip: you can skip this step if you're only running social DM campaigns. 3. **connect a social account (optional)** - connect Instagram or Twitter from settings → connected accounts to enable DM outreach. iris uses the same account you sign in with on each platform. 4. **create your first campaign** - hit 'new campaign' and pick an objective: book DJ gigs, host events, promote a product, or custom outreach. fill in the campaign brief - iris uses it to generate target profiles and write drafts. 5. **run discovery and review drafts** - iris finds matching contacts, scores them by fit, and drafts a personalized message for each. open the review queue, edit anything that doesn't sound like you, approve, and send. tip: you don't have to send every draft. select the ones you want; the rest stay in the queue or get discarded. ### build your first campaign a step-by-step walkthrough of the campaign wizard from objective to first reply. difficulty: beginner | estimated time: 10 minutes **steps:** 1. **pick an objective** - the wizard's first screen asks what you're trying to do. pick one of: book DJ gigs, host events, promote a product, or custom outreach. the choice changes which discovery sources iris searches, what tone the drafts take, and which channels are recommended. 2. **fill in the campaign brief** - the second screen depends on the objective. for product promotion, you paste the product URL or write a description and iris analyzes it to generate target profiles. for venue objectives, you pick the city and venue type. you can adjust everything before discovery runs. 3. **run discovery** - iris searches Instagram and TikTok (plus public business listings for venue campaigns) for contacts matching the target profile. each result gets enriched with a name, role, niche, social handles, an email if one's listed publicly, and a fit score from 0-100. tip: filter by fit score or hand-select rows. iris only sends to contacts you mark as 'in the campaign'. 4. **configure outreach settings** - choose which channels to use (email + Instagram, Twitter only, etc.), how many follow-ups to send, and how long to wait between them. defaults are conservative on purpose. 5. **review the campaign and activate** - the review screen shows everything: contact count, channel mix, expected message volume, and your usage against plan limits. hit activate and iris drafts a message for every contact in the background. 6. **approve drafts in the review queue** - once drafts are ready (usually a few minutes for small campaigns), open the review queue. read each draft, edit anything that's off, approve to send, or regenerate to get a different angle. 7. **track replies in the inbox** - approved messages send with humanized pacing. replies land in the inbox within seconds for email, slightly longer for social DMs. iris drafts the next message; you approve. ### verify a sender email verify the address you want emails to come from so iris can send on your behalf. difficulty: beginner | estimated time: 2 minutes **steps:** 1. **open settings → channels** - scroll to the 'sender email' card. enter the address you want emails to come from (e.g. you@yourcompany.com) and the display name recipients will see. tip: use a custom-domain address rather than gmail, yahoo, or outlook. free providers fail DMARC checks and degrade deliverability. 2. **click the verification link in your inbox** - SendGrid emails the address you entered with a verification link. click it once to confirm you own the address. if the address is already verified at the SendGrid account level, iris will skip the email step and mark it verified immediately. 3. **confirm the badge flips to verified** - return to settings → channels. the sender card should show a green 'verified' badge. if it still says 'pending', press 'check status' or click the email link again. 4. **you're ready to send** - every email iris sends will now use your verified address as the from-name and reply-to. activate a campaign and drafts will populate and send from your address as soon as you approve them. ### connect your Instagram account enable Instagram DM outreach by connecting your account from settings. difficulty: beginner | estimated time: 5 minutes **steps:** 1. **open settings → connected accounts** - the connected accounts page lists every channel iris supports and the connection state for each. 2. **click connect Instagram** - iris redirects you to Instagram to sign in. you're signing in with your own Instagram account - iris doesn't ask for your password and never sees it. 3. **approve the messaging permission** - Instagram shows you what permission iris is requesting (the ability to send and receive DMs on your behalf) and asks you to approve. you can revoke this at any time from your Instagram settings. tip: if you have multiple Instagram accounts, make sure you're signed in to the right one before clicking connect. 4. **confirm the connection** - iris redirects you back. the connected accounts page should now show Instagram as 'connected' with your handle. 5. **send a test DM** - before running a campaign, send a DM to a friend's account or your own alt to confirm it works. you'll see the test message arrive on Instagram and the read receipt come back into the iris inbox. ### connect your Twitter account enable Twitter DM outreach by connecting your account from settings. difficulty: beginner | estimated time: 5 minutes **steps:** 1. **open settings → connected accounts** - the same page that handles Instagram - find the Twitter row and click connect. 2. **sign in to Twitter** - iris redirects to Twitter. sign in with the account you want iris to send DMs from. iris doesn't see your Twitter password. 3. **approve the messaging permission** - Twitter shows you what iris is asking for: the ability to send and receive DMs on your account. approve to continue. 4. **confirm the connection** - after redirecting back, the connected accounts page shows Twitter as 'connected' with your handle. 5. **send a test DM** - send a test DM to a second account you control. confirm the send goes through, the message appears in the recipient's inbox, and any reply lands back in the iris inbox. tip: Twitter has a 'message request' folder for DMs from accounts you don't follow. test against an account that follows yours so you don't fight the message-request UX. ### run a product promotion campaign let iris analyze your product, generate target profiles, and discover the right contacts to pitch. difficulty: intermediate | estimated time: 15 minutes **steps:** 1. **create a campaign and pick 'promote a product'** - this objective triggers the AI product analyzer. you don't need to know your target audience up front - iris will propose one based on the product itself. 2. **paste your product URL or write a description** - the more iris can read, the better the target profiles will be. a public product page works best; a one-paragraph description works fine if the product isn't public yet. 3. **review the generated target profiles** - iris generates 2-4 target profiles, each with a name, profile type (creator, B2B professional, agency, etc.), job titles, industries, pain points, and recommended channels. read them critically - they're starting points, not final. tip: if a profile feels off, edit the keywords or job titles directly. small changes make a big difference at discovery time. 4. **select the profiles to discover against** - you can run discovery against one profile at a time or all of them. start with the highest-fit profile and add more if the first batch is too small. 5. **review and refine the contact list** - discovery returns enriched contacts scored by fit. filter by score, hand-deselect anyone who doesn't fit, and confirm the channels each contact is reachable on. 6. **review drafts and send** - iris drafts a message per contact that leads with how the product fits the recipient's work, not with a feature dump. approve in the review queue and send. ### use the review queue the four things you can do with a drafted message: approve, edit, regenerate, or skip. difficulty: beginner | estimated time: 5 minutes **steps:** 1. **open the review queue** - every active campaign has a queue of drafts waiting for approval. find it in the campaign detail page or in the global 'pending review' tab. 2. **approve as-is** - if a draft reads well, hit approve. iris schedules the send with humanized pacing and updates the conversation status. tip: you can bulk-approve - select multiple drafts and hit approve all. iris paces the actual sends regardless of how fast you approve. 3. **edit inline** - if a draft is mostly right but needs a tweak, click edit. you can change the subject, the body, or both. iris remembers your edits when drafting future messages in the same campaign. 4. **regenerate** - if the angle is off, hit regenerate. iris re-drafts from scratch with the same brief but a different opener, structure, or angle. you can regenerate up to your plan's per-campaign limit. 5. **skip** - if a contact slipped through discovery and they shouldn't be in the campaign, hit skip. they stay on the contact list but no message ever goes out. ### call iris from an AI agent run iris campaigns programmatically using x402 USDC payments or MPP credit packs. difficulty: advanced | estimated time: 20 minutes **steps:** 1. **decide on a payment rail** - x402 (USDC on Base) charges per-use - good for autonomous agents that don't have a Stripe account. MPP (Stripe credits) buys credits in packs of 5 campaigns - good for agents owned by a human or company with billing already set up. 2. **send a request without a payment header** - call any paid endpoint (e.g. POST /campaigns) without payment. iris responds with HTTP 402 and a JSON body that lists every payment option, the price for each, and the URL to acquire credits. the response is the spec - your agent reads it and picks a rail. tip: this is the standard 402 discovery flow. you don't need to bake pricing into the agent at build time. 3. **settle the payment** - for x402, attach a USDC payment header on the next request. for MPP, buy credits via the URL in the 402 response, then call the endpoint with the API key from the credit purchase. 4. **run the campaign** - once payment settles, the original request runs. you'll get back a campaign ID, contact count, and a status URL. poll the status URL to track discovery and drafting progress. 5. **approve drafts via the API** - the agent endpoints expose the same draft approval flow as the UI: list pending drafts, approve, edit, regenerate, or skip. the inbox endpoints expose replies threaded by contact. ## comparisons ### iris vs Apollo.io iris focuses on quality conversations; Apollo focuses on database scale. **differentiators:** - drafted per contact, not templated: iris reads each contact's profile and writes a fresh message against your campaign brief. Apollo sequences run on liquid templates with merge tokens - faster to set up, but readers can usually tell. - multi-channel from one inbox: iris sends across email, Instagram, and Twitter, threading every reply back into one conversation per contact. Apollo is email-first; social channels are not the core flow. - discovery is fit-scored, not unbounded: iris discovery returns 20-200 enriched contacts with a fit score. Apollo returns thousands and asks you to filter. opposite philosophies. - no contact-data resale model: iris uses public data sources (public Instagram and TikTok profiles, plus public business listings). it doesn't ship with a proprietary B2B database, so there's no list-buying step before you can send. **verdict:** pick Apollo if you want a giant database to mine and you're running templated sequences at high volume. pick iris if your sales motion depends on the message being right, not just the list being big - and if you want social channels in the same inbox as email. ### iris vs Lemlist iris drafts from scratch; Lemlist personalizes a template. **differentiators:** - AI-drafted bodies, not just personalized variables: Lemlist's variables (first name, company, custom images) sit inside a static template body. iris generates the entire body per contact - structure, opener, and angle vary, not just the merge fields. - Instagram and Twitter DMs, not just email: Lemlist added LinkedIn over time but stayed primarily email. iris treats Instagram and Twitter DMs as first-class channels with their own drafting tone. - discovery + sending in one tool: Lemlist focuses on the sequencer; you bring the contact list. iris discovers contacts, enriches them, and drafts messages in one campaign flow. - approval queue by default: iris drafts then waits for your approval. Lemlist's typical flow auto-sends once the sequence is activated. **verdict:** pick Lemlist if image personalization and LinkedIn are central to your motion and you already have a list. pick iris if your channels are email + Instagram + Twitter, you want messages drafted from scratch rather than templated, and you want discovery in the same tool. ### iris vs Instantly iris is built around quality; Instantly is built around volume. **differentiators:** - one verified sender, not inbox rotation: iris sends from your verified sender email only. Instantly's model rotates across many sender mailboxes to spread volume - effective at scale, but a different deliverability strategy than iris is set up for. - AI-drafted per contact, not high-volume sequences: Instantly is optimized for sending the same sequence to many contacts. iris is optimized for drafting different messages for each contact, then sending fewer of them. - social DMs in the same inbox: Instantly is email-only. iris threads email, Instagram, and Twitter conversations together per contact. - no warm-up tooling: iris doesn't ship inbox warm-up because the volumes iris sends don't require it. if you need warm-up, you probably need a different tool. **verdict:** pick Instantly if your motion is high-volume cold email and deliverability tooling is what you came for. pick iris if you'd rather send 50 hand-edited drafts than 5,000 templated emails, and want social channels in the same inbox. ### iris vs Hunter iris combines discovery, drafting, and sending; Hunter is mostly the finder. **differentiators:** - discovery is multi-source, not domain-based: Hunter searches for emails associated with a domain. iris discovers contacts by who they are (creator, founder, venue manager) across Instagram and TikTok (plus public business listings for venues), then enriches with whatever public data exists. - drafting and sending built in: Hunter's campaigns feature is functional but minimal. iris drafts each message per contact and runs the approval-and-send queue as a first-class flow. - social DMs, not just email: Hunter is an email tool. iris reaches the same contact on email, Instagram, or Twitter depending on which channel they're most reachable on. - fit-scored, not lookup-based: Hunter is built for known-domain lookups. iris is built for 'I know who I want to reach but not who they are yet' discovery, scored by fit against a target profile. **verdict:** pick Hunter if you primarily need email addresses for a known list of domains and you'll send from somewhere else. pick iris if you want discovery + drafting + sending across email + social in one tool, with messages written per contact instead of templated. ### iris vs the DIY stack iris vs Apollo + ChatGPT + Mailchimp + your social tab. **differentiators:** - one tool, one inbox: the DIY stack lives across at least four tools. iris keeps discovery, drafting, sending, and reply tracking in one place, with conversations threaded per contact regardless of channel. - drafting wired to discovery: in the DIY stack, ChatGPT doesn't know who's on your Apollo list. iris drafts each message with the discovery context already attached - role, recent activity, fit signals - so the message reads like you actually looked at the contact. - no copy-paste between tools: every handoff in the DIY stack is a place data goes missing - export to CSV, paste into prompt, copy back, push to mailer, click around to find the reply. iris removes the handoffs. - social DMs natively: the DIY stack treats Instagram and Twitter as 'the social tab' you check between emails. iris treats them as channels with the same drafting and reply-tracking as email. **verdict:** the DIY stack gives you flexibility and low upfront cost - if you're sending small volumes and don't mind the context-switching, it works. iris is for when the handoffs become the bottleneck: you're losing track of replies, your drafts are generic because ChatGPT doesn't have the contact context, and your inbox is three browser tabs deep. ## agent payment protocols AI agents can use iris without an API key via credits purchased with x402 (crypto) or MPP (fiat). 1 credit = 1 campaign with 30 contacts and 240 messages; AI drafting and response analysis are bundled in. ### how it works 1. pay per-campaign (x402 only): read the PAYMENT-REQUIRED header from the 402, sign it, and replay `POST /campaigns` with a PAYMENT-SIGNATURE header (x402 v2; $2.25 per campaign) 2. create campaigns - each campaign costs 1 credit 3. discovery, AI drafting, and message sends run automatically inside the campaign workflow as internal QStash jobs - agents do not call /discovery/search, /ai/generate, or /channels/send directly (those endpoints require an API key) 4. check balance: `GET /agent/credits` 5. credits never expire. refunded if campaign creation fails. ### pricing x402: $2.25 per-campaign (pay inline with a PAYMENT-SIGNATURE header, x402 v2). MPP: credit packs only (5 for $12.00 via POST /agent/credits). when no credits are available, iris returns 402 with purchase info. ## FAQ Q: how do I create my first campaign? A: open the campaigns page and click 'create campaign'. choose your objective type (venue booking, product promotion, or custom outreach), fill in the details, discover contacts, and activate. iris drafts personalized messages for each contact using AI. Q: how does contact discovery work? A: iris finds leads on Instagram and TikTok, plus public business listings for venue and location-based campaigns. specify your target profile, location, and filters, and iris finds relevant contacts with emails and social handles for your campaigns. Q: how do I connect my social accounts? A: go to settings and click 'connected accounts'. you can link Twitter and Instagram via OAuth. once connected, iris can send DMs and track conversations across all channels. Q: do I need any technical skills to use iris? A: no. the entire app is point-and-click - you describe who you want to reach, iris finds them, drafts messages, and routes replies. the API and MCP server are available if you want to script things, but they're optional. if you can use Gmail you can use iris. Q: how is iris different from Apollo, Lemlist, or Instantly? A: iris drafts every message from scratch per contact instead of personalizing a shared template, treats Instagram and Twitter DMs as first-class channels alongside email, and threads every reply into one inbox per contact. see the comparison pages at /compare for side-by-side breakdowns. Q: what are objective types? A: campaigns are driven by objectives: 'book DJ gigs' and 'host events' for venue outreach, 'promote a product' for AI-analyzed product marketing with auto-generated target profiles, and 'custom outreach' for your own criteria. iris infers the right objective from your goal description in chat - you don't pick it manually. Q: how does AI message generation work? A: when you activate a campaign, iris uses Claude to draft personalized emails for each contact. it considers the contact's business, your campaign objective, and any product analysis to write relevant outreach. you review and approve drafts before they're sent. Q: can I customize email templates? A: yes. use variables like {{business_name}} and {{contact_name}} to personalize. you can also let the AI generate templates based on your campaign objective and target audience. Q: how many contacts can I target per campaign? A: campaign size scales with your plan. starter campaigns hold 30 contacts, pro hold 40, premium hold 60. each contact gets about 8 message slots (initial outreach plus follow-ups across email, Instagram, and Twitter), which is why a 30-contact campaign carries 240 messages. Q: can I pause or cancel a campaign mid-send? A: yes. open the campaign, hit pause, and iris stops queueing new drafts immediately. anything already in the review queue stays there - you can approve, edit, or discard. resume the campaign anytime; it picks up where it left off. Q: how do I duplicate a successful campaign? A: from the campaign detail page, click 'duplicate'. iris copies the objective, brand voice, target profile, and channel settings into a fresh campaign with no contacts attached, so you can run discovery against a new region or audience without rebuilding the brief. Q: how do I import existing contacts? A: go to contacts and click 'import'. upload a CSV with columns for business name, email, phone, website, Instagram, and address. download the template first to get the correct format. Q: what data sources does discovery use? A: iris discovers contacts on Instagram and TikTok, plus public business listings for venue and location-based campaigns. social sources give handles and follower counts; business listings give addresses and phone numbers. Q: how does fit scoring work? A: every discovered contact gets a 0-100 fit score against your campaign brief. iris weighs the match between the contact's profile (business type, location, social signals, listed services) and your objective + target profile. you can sort or filter the discovery results by fit score before adding contacts to a campaign. Q: can I exclude contacts I've already messaged? A: yes. iris dedupes against your contact history so a contact already reached in a prior campaign isn't silently re-added to a new one. you can also unselect contacts in the discovery review step before activating a campaign. Q: is the contact data legal to use? A: iris pulls from public sources (public Instagram and TikTok profiles, public business listings, company websites). every outreach email includes a per-contact unsubscribe link as required by CAN-SPAM, and one click flips the contact's status to opted_out across email, Instagram, and Twitter so iris stops sending on every channel. for GDPR jurisdictions you remain the data controller; keep your campaign briefs compliant with local rules. Q: what channels does iris support? A: email (via SendGrid), Instagram DMs, and Twitter DMs. each channel has its own adapter for formatting, rate limits, and delivery tracking. the orchestrator picks the best channel per contact based on available handles. Q: what happens when someone replies? A: replies are tracked in the unified inbox. email replies come through SendGrid inbound webhooks, Twitter and Instagram DMs come through platform webhooks. AI can help draft responses, which you review before sending. Q: what is the email sending limit? A: message limits are per campaign, not global. starter: 240 messages per campaign, pro: 320, premium: 480. each campaign's cap scales with its contact size (8× contacts) to cover initial outreach plus follow-ups across email, instagram, and twitter. the number of campaigns you can create per billing period determines total capacity. Q: what is humanized pacing? A: iris staggers sends with a randomized delay between each message instead of blasting an entire campaign at once. delays are tuned per channel: email is the fastest, Instagram and Twitter DMs are slower to stay well under platform thresholds. the goal is a cadence that doesn't trip platform anti-spam. Q: do you support LinkedIn? A: not currently. iris sends across email, Instagram DMs, and Twitter DMs. LinkedIn outreach isn't supported yet because LinkedIn's API restricts third-party messaging to specific partner programs. Q: how do I tune the tone of AI drafts? A: the campaign brief drives the tone. when you describe the campaign objective and target audience, iris uses that context (alongside the contact's profile) when prompting Claude. there is no separate per-contact 'persona' field - the campaign brief is the lever. Q: what happens to bounced emails? A: iris records hard and soft bounces from SendGrid webhooks. hard bounces (invalid address) mark the contact as undeliverable so iris stops sending to that address. soft bounces (full inbox, temporary delivery failure) are recorded but don't block future sends. bounce status shows next to each contact in the conversation view. Q: how does iris handle email deliverability? A: iris sends from your verified sender email through SendGrid and paces sends with randomized delays between messages. you keep ownership of your sender reputation: iris doesn't pool many users' sends through one shared address. Q: what stops my emails from landing in spam? A: the biggest factors are sender authentication (SPF/DKIM/DMARC on your domain), the content itself (spam-trigger words, excessive links, large images), and engagement signals (opens, replies, low spam complaints). iris drafts plain conversational text by default and embeds a single unsubscribe link, both of which help. set up SPF/DKIM through the verify-sender-email guide before running a real campaign. Q: what is the unsubscribe flow? A: every iris outreach email contains a per-contact unsubscribe link in the footer. one click flips the contact's status to 'opted_out' and stops every future send across all channels - email, Instagram DM, and Twitter DM - automatically. the contact stays in your database for audit purposes but is excluded from all campaign send queues. Q: what plans are available? A: iris offers starter, pro, and premium plans with monthly and yearly billing. each plan caps the number of campaigns and the size of each campaign (contacts and messages); AI drafting and response analysis are bundled in. see the pricing page for current rates. Q: is there a free plan? A: yes. the free tier includes one campaign per month so you can run a real outreach end-to-end before paying. AI drafting, discovery, and the unified inbox all work on free - the cap is on campaign volume, not on the features you can try. Q: what counts as a campaign for billing? A: a campaign is a single outreach project with one objective and one target audience. activating a campaign consumes one of your monthly campaign credits. duplicating a campaign without activating it is free; only activation counts. saved drafts and paused campaigns don't consume additional credits. Q: do unused campaigns roll over? A: no. campaign credits reset at the start of each billing period and don't accumulate. if you need more campaigns in a given month, upgrade temporarily - changes take effect immediately and prorate against the period. Q: how do I upgrade or cancel? A: go to settings > billing to manage your subscription. you can upgrade, downgrade, or cancel anytime. cancellations take effect at the end of your current billing period. Q: does iris have an API? A: yes. iris has a REST API at api.iris-ai.dev with 80+ endpoints covering campaigns, contacts, conversations, messages, discovery, AI, analytics, billing, and webhooks. authenticate with API keys (is_live_...) or agent payment protocols (x402, MPP). Q: how do I get an API key? A: go to settings > API keys and click 'generate key'. choose the scopes you need (read, write, generate, send, delete, admin) and set an expiration. the raw key is only shown once, so save it securely. Q: what are x402 and MPP? A: x402 and MPP are agent payment protocols that let AI agents use the iris API without an API key. x402 agents pay per-campaign ($2.25). MPP agents buy credit packs (5 for $12.00). each campaign credit covers the starter envelope: 30 contacts and 240 messages, with AI drafting bundled in. credits never expire and are refunded if campaign creation fails. Q: is there an MCP server for AI agents? A: yes. the iris MCP server exposes campaigns, contacts, discovery, drafting, and analytics as MCP tools that Claude Code, Cursor, and other MCP clients can call directly. installation and tool reference live at /docs in the iris app. Q: where is the full API documented? A: /docs in the iris app has the full reference - resources, endpoints, parameters, request/response schemas, error codes, code examples, and rate limits. agent-readable formats are available at /llms.txt, /llms-full.txt, and /openapi.json. the .well-known/x402 endpoint advertises pricing for x402 agents. Q: can I receive webhooks for inbound replies? A: yes. configure a webhook subscription at /api/v1/webhooks and iris pushes events for inbound messages, campaign state changes, message status updates, and discovery completions to your endpoint. each delivery is signed with HMAC so you can verify it's from iris. Q: how do I track campaign performance via API? A: the analytics endpoints provide open rates, click rates, response rates, and conversions. filter by date range and campaign. webhook subscriptions can push events to your server in real-time. Q: my campaign drafts aren't generating, what's wrong? A: drafts run through QStash as a background job after activation, so there can be a short delay. if drafts haven't appeared after 5 minutes, check (1) the campaign has at least one selected contact, (2) your AI generation usage hasn't hit the plan limit, and (3) the activity feed on the campaign page for any error messages. if the issue persists, contact support with the campaign ID. Q: a contact replied but I can't see it in the inbox A: for email, check that the verified sender's reply-to domain is configured to forward to iris's inbound webhook (the verify-sender-email guide covers this). for Instagram and Twitter DMs, the inbound webhook needs the platform's app review approval, which iris handles - if a known-good reply is missing, contact support and include the platform handle and approximate timestamp. Q: email sender verification is stuck on 'pending' A: SendGrid emails the verification link to the address you added, not to your iris account. check that mailbox (and spam folder) and click the link. if the link expired, return to settings > channels and click 'resend verification'. verification is a one-time step per sender address. Q: a connected social account is showing as disconnected A: OAuth tokens expire when the platform rotates them or you revoke iris from your platform settings. go to settings > connected accounts, click 'reconnect' on the affected platform, and complete the OAuth flow again. iris re-encrypts and stores the new token automatically. ## blog ### introducing iris - the AI messenger for outbound published: 2026-04-15 | 4 min read iris finds the right contacts, drafts a personal message for every one, and routes replies across email, Instagram, and Twitter into a single inbox. read more: https://www.iris-ai.dev/blog/introducing-iris ### why personalization beats templates - even when the template is good published: 2026-04-08 | 5 min read merge tokens fool no one. the math on why one-to-one drafts outperform variant templates, and how iris closes the gap. read more: https://www.iris-ai.dev/blog/personalization-vs-templates ### the unified inbox - email, Instagram, and Twitter in one thread published: 2026-04-01 | 4 min read why running outreach across three channels falls apart without conversation threading, and how iris keeps everything in one view. read more: https://www.iris-ai.dev/blog/unified-inbox ### how iris finds the right contacts published: 2026-03-22 | 5 min read discovery isn't a list - it's a search. how iris turns a campaign objective into target profiles, search queries, and enriched contact rows. read more: https://www.iris-ai.dev/blog/contact-discovery ### what actually makes a cold email get a reply published: 2026-03-12 | 6 min read five things that move reply rate, ranked by how much. and three things people obsess over that don't. read more: https://www.iris-ai.dev/blog/what-makes-a-cold-email-reply ### agent payments - per-campaign pricing for AI agents published: 2026-03-01 | 4 min read iris is callable as an API. AI agents pay per campaign with USDC (x402) or Stripe credits (MPP), so they only pay for what they use. read more: https://www.iris-ai.dev/blog/agent-payments ### objective-driven campaigns - what the wizard actually does published: 2026-02-20 | 5 min read book DJ gigs, host events, promote a product, or define custom outreach. each objective changes how iris discovers, drafts, and paces. read more: https://www.iris-ai.dev/blog/objective-driven-campaigns --- # API documentation ## base URL ``` https://api.iris-ai.dev ``` ## authentication all requests require a bearer token: ``` Authorization: Bearer is_live_... ``` create API keys at https://www.iris-ai.dev/settings under the API section. ## scopes API keys support scoped permissions. assign only the scopes your integration needs. | scope | description | |-------|-------------| | read | view all resources (campaigns, contacts, conversations, messages, analytics, billing) | | write | create, update, and soft-delete business data (campaigns, contacts, conversations, notifications) | | generate | costly external API calls (Claude AI generation, discovery searches) | | send | send and approve outbound messages (email, Instagram DM, Twitter DM) | | admin | manage API keys and webhooks | default scopes (if none specified, or `["*"]`): all scopes: `read`, `write`, `generate`, `send`, `admin`. UI default for new keys is `read, write, generate, send` (no `admin`). ## endpoints ### authentication verify your API key and view its metadata. #### GET /keys/me get information about the currently authenticated API key **response (200):** ```json { "success": true, "data": { "id": "uuid", "user_id": "uuid", "name": "production", "key_prefix": "is_live_a1b2c3d4", "scopes": ["*"], "last_used_at": "2024-01-15T10:30:00Z", "expires_at": null, "revoked_at": null, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-15T10:30:00Z" } } ``` ### API keys programmatically manage API keys. all routes in this group (except `/keys/me`) require an API key with the `admin` scope. `/keys/me` returns metadata for the currently-authenticated key and works without any scope. #### GET /keys list all API keys for your account **query parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | page_size | integer | no | items per page (default: 50) | **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "user_id": "uuid", "name": "production", "key_prefix": "is_live_a1b2c3d4", "scopes": ["*"], "last_used_at": "2024-01-15T10:30:00Z", "expires_at": null, "revoked_at": null, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-15T10:30:00Z" } ], "pagination": { "page": 1, "page_size": 50, "total": 1, "total_pages": 1 } } ``` #### POST /keys create a new API key. the raw key is only returned once. **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | name | string | yes | key name (1-100 characters) | | scopes | string[] | no | permission scopes (any of: read, write, delete, generate, send, admin). defaults to ["*"] for full access | | expires_at | ISO 8601 | no | optional expiration date | **response (200):** ```json { "success": true, "data": { "id": "uuid", "user_id": "uuid", "name": "staging", "key_prefix": "is_live_e5f6g7h8", "key": "is_live_e5f6g7h8a9b0c1d2e3f4g5h6i7j8k9l0m1n2o3p4", "scopes": ["read"], "last_used_at": null, "expires_at": null, "revoked_at": null, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } } ``` **notes:** - the raw key value is only returned on creation - store it securely #### PATCH /keys/:id rename an API key. only the display label is updated; the secret, scopes, and expiry are untouched. **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | name | string | yes | new display name (1-100 chars) | **response (200):** ```json { "success": true, "data": { "id": "uuid", "name": "renamed key", "key_prefix": "is_live_abcd", "scopes": ["read", "write"], "last_used_at": "2026-04-29T12:00:00Z", "expires_at": null, "revoked_at": null, "created_at": "2026-04-01T12:00:00Z", "updated_at": "2026-04-29T13:00:00Z" } } ``` **errors:** - 404 APIKEY_006: API key not found #### DELETE /keys/:id revoke an API key. this action is irreversible. **response:** 204 no content **errors:** - 404 APIKEY_006: API key not found #### POST /keys/:id/rotate rotate an API key: revokes the old key and creates a new one with the same name and scopes. the new raw key is returned exactly once. update stored credentials immediately. the old key stops authenticating as soon as this returns. cannot rotate the key being used for the request itself (would lock the caller out mid-rotation). **response (200):** ```json { "success": true, "data": { "id": "uuid", "name": "my integration", "key_prefix": "is_live_xyz...", "scopes": ["read", "write", "generate", "send"], "last_used_at": null, "expires_at": null, "revoked_at": null, "created_at": "2026-05-07T12:00:00Z", "updated_at": "2026-05-07T12:00:00Z", "key": "is_live_xyz...thefullnewkeyshownonceonly" } } ``` **errors:** - 404 APIKEY_006: API key not found - 422 SERVER_005: cannot rotate the API key currently being used **notes:** - the new key's `id` is different from the old `id`. update any stored references. - the underlying service makes the usage counter net-zero, so rotation never trips MAX_ACTIVE_KEYS_PER_USER even when the user is at the limit. ### campaigns create and manage outreach campaigns. #### GET /campaigns list all campaigns **scope:** `read` **query parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | page_size | integer | no | items per page (default: 50) | | status | string | no | filter by status (draft, active, paused, completed) | **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "user_id": "uuid", "name": "Q1 outreach", "status": "active", "objective_type": "product_promotion", "product_name": "Acme Widget", "product_url": "https://acme.example.com", "product_description": null, "follow_up_enabled": true, "follow_up_max_count": 2, "follow_up_delay_days": 3, "keywords": ["coffee shop", "cafe"], "venue_types": [], "outreach_channels": ["email"], "location": "Brooklyn, NY", "radius_miles": 25, "min_budget": null, "max_budget": null, "dynamic_fields": {}, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } ], "pagination": { "page": 1, "page_size": 50, "total": 1, "total_pages": 1 } } ``` #### POST /campaigns create a new campaign **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | name | string | yes | campaign name | | objective_type | string (1-100 chars) | yes | free-text 2-5 word goal summary (e.g. 'venue_host', 'promote my coffee shop'). discovery routing uses target_profile_type, not this field. | | product_name | string | no | product name (for product_promotion objective) | | product_url | url | no | product URL for AI analysis | | product_description | string | no | product description for AI context | **response (200):** ```json { "success": true, "data": { "id": "uuid", "name": "spring outreach", "objective_type": "product_promotion", "status": "draft", "created_at": "2024-01-15T10:30:00Z" } } ``` **errors:** - 402 PAYMENT_005: insufficient credits (1 credit required) - purchase credits at POST /agent/credits - 429 CAMP_009: campaign limit reached for billing period **notes:** - agent payments: this is the only paid operation in the public API for agents. x402 agents pay $2.25 USDC per campaign. read the PAYMENT-REQUIRED header from the 402, sign it, and replay with a PAYMENT-SIGNATURE header (x402 v2). MPP agents deduct 1 credit from a credit pack ($2.40 effective). discovery, AI drafting, and message sends run automatically inside the campaign workflow as internal QStash jobs. agents never call /discovery/search, /ai/generate, or /channels/send directly (those endpoints require an API key). - iris does not offer an x402 free tier - every agent campaign requires payment. - see the agent-payments and credit-packs sections for the full 402 challenge flow. #### GET /campaigns/:id get a specific campaign with full details **scope:** `read` **errors:** - 404 CAMP_001: campaign not found #### PATCH /campaigns/:id update a campaign **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | name | string | no | campaign name | | status | "draft" | "active" | "paused" | "completed" | no | campaign status | | product_name | string | no | product name | | product_url | url | no | product URL | **errors:** - 404 CAMP_001: campaign not found - 400 CAMP_008: invalid status transition #### DELETE /campaigns/:id soft-delete a campaign (sets status to 'deleted') **scope:** `write` **response:** 204 no content **errors:** - 404 CAMP_001: campaign not found #### GET /campaigns/:id/contacts list contacts in a campaign **scope:** `read` **query parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | page_size | integer | no | items per page (default: 50) | | status | string | no | filter by contact status | **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "user_id": "uuid", "email": "jane@example.com", "first_name": "Jane", "last_name": "Smith", "company_name": "Acme Inc", "job_title": "Head of Marketing", "phone": null, "website": null, "tags": ["lead", "enterprise"], "enrichment_data": null, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z", "deleted_at": null, "campaign_contact_id": "uuid", "campaign_contact_status": "active", "added_at": "2024-01-15T10:30:00Z" } ], "pagination": { "page": 1, "page_size": 50, "total": 1, "total_pages": 1 } } ``` **errors:** - 404 CAMP_001: campaign not found #### GET /campaigns/:id/usage get campaign usage statistics **scope:** `read` **response (200):** ```json { "success": true, "data": { "campaign_id": "uuid", "status": "active", "total_contacts": 150, "total_conversations": 42, "delivery_status": { "sent": 42, "delivered": 40, "opened": 28, "clicked": 12 } } } ``` **errors:** - 404 CAMP_001: campaign not found #### POST /campaigns/:id/analyze trigger AI product analysis for a campaign **scope:** `write` **errors:** - 404 CAMP_001: campaign not found - 429 AI_004: AI rate limit exceeded **notes:** - analyzes the campaign's product URL/description and generates target profiles - returns immediately - analysis runs asynchronously #### GET /campaigns/:id/target-profiles list target profiles for a campaign **scope:** `read` **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "campaign_id": "uuid", "name": "SaaS founders", "profile_type": "b2b_professional", "fit_score": 85, "job_titles": ["CEO", "CTO", "founder"], "industries": ["technology", "SaaS"], "discovery_keywords": ["saas founder", "tech startup"], "recommended_channels": ["email", "twitter"] } ] } ``` **errors:** - 404 CAMP_001: campaign not found #### POST /campaigns/:id/target-profiles create a target profile for a campaign **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | name | string | yes | profile name | | profile_type | "local_business" | "content_creator" | "influencer" | "b2b_professional" | "ecommerce" | "saas_company" | "agency" | "freelancer" | "other" | yes | target profile type | | job_titles | string[] | no | target job titles | | industries | string[] | no | target industries | | discovery_keywords | string[] | no | keywords for contact discovery | | recommended_channels | string[] | no | recommended outreach channels | **errors:** - 404 CAMP_001: campaign not found #### DELETE /campaigns/:id/target-profiles/:profileId delete a target profile **scope:** `write` **response:** 204 no content **errors:** - 404 CAMP_001: campaign or profile not found ### audiences your reusable audience library - target profiles created once (manually or AI-generated from a product) and attached to any campaign. attaching copies the profile into the campaign, so campaign edits never touch the library master. #### GET /audiences list your audience library (newest first) **scope:** `read` **query parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | page_size | integer | no | items per page (default: 50) | **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "name": "indie coffee shops", "description": "independent cafes with an active local following", "profile_type": "local_business", "fit_score": 82, "job_titles": ["owner", "general manager"], "industries": ["food and beverage", "hospitality"], "company_sizes": ["1-10"], "pain_points": ["slow weekday mornings", "no online ordering"], "buying_signals": ["hiring baristas", "new location announcement"], "discovery_sources": ["google_maps", "instagram"], "discovery_keywords": ["specialty coffee", "third wave cafe"], "outreach_angle": "help them turn quiet weekday mornings into regulars", "contacts_discovered": 24, "created_at": "2026-07-01T00:00:00Z", "updated_at": "2026-07-09T14:30:00Z" } ], "pagination": { "page": 1, "page_size": 50, "total": 1, "total_pages": 1 } } ``` #### POST /audiences create a library profile manually. names are unique per library (case-insensitive). **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | name | string | yes | audience name (1-100 chars) | | profile_type | "local_business" | "content_creator" | "influencer" | "b2b_professional" | "ecommerce" | "saas_company" | "agency" | "freelancer" | "other" | yes | target profile type | | description | string | no | who this audience is (max 500 chars) | | fit_score | integer (0-100) | no | how well this audience fits your product | | job_titles | string[] | no | target job titles (max 10) | | industries | string[] | no | target industries (max 10) | | pain_points | string[] | no | problems they feel (max 10) | | buying_signals | string[] | no | signals they're ready to buy (max 10) | | discovery_keywords | string[] | no | keywords for contact discovery (max 10) | | outreach_angle | string | no | the hook messages should lead with (max 500 chars) | **response (201):** ```json { "success": true, "data": { "id": "uuid", "name": "indie coffee shops", "description": "independent cafes with an active local following", "profile_type": "local_business", "fit_score": 82, "job_titles": ["owner", "general manager"], "industries": ["food and beverage"], "company_sizes": [], "pain_points": ["slow weekday mornings"], "buying_signals": [], "discovery_sources": [], "discovery_keywords": ["specialty coffee"], "outreach_angle": "help them turn quiet weekday mornings into regulars", "contacts_discovered": 0, "created_at": "2026-07-09T14:30:00Z", "updated_at": "2026-07-09T14:30:00Z" } } ``` **errors:** - 400 SERVER_002: an audience with this name already exists - 422 SERVER_005: validation failed #### GET /audiences/:id get one library profile **scope:** `read` **response (200):** ```json { "success": true, "data": { "id": "uuid", "name": "indie coffee shops", "description": "independent cafes with an active local following", "profile_type": "local_business", "fit_score": 82, "job_titles": ["owner", "general manager"], "industries": ["food and beverage"], "company_sizes": ["1-10"], "pain_points": ["slow weekday mornings"], "buying_signals": ["hiring baristas"], "discovery_sources": ["google_maps"], "discovery_keywords": ["specialty coffee"], "outreach_angle": "help them turn quiet weekday mornings into regulars", "contacts_discovered": 24, "created_at": "2026-07-01T00:00:00Z", "updated_at": "2026-07-09T14:30:00Z" } } ``` **errors:** - 404 SERVER_003: audience not found #### DELETE /audiences/:id delete a library profile. campaigns it was attached to keep their copy (those use the campaign target-profile DELETE). **scope:** `write` **response:** 204 no content **errors:** - 404 SERVER_003: audience not found #### POST /audiences/:id/attach copy a library profile into a campaign. the response is the campaign's own copy (a TargetProfile) - editing it never touches the library master. if the campaign already has a profile with the same name, the existing copy is returned instead. **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | campaign_id | uuid | yes | the campaign to attach to (must be yours and not completed or deleted) | **response (201):** ```json { "success": true, "data": { "id": "uuid", "campaign_id": "uuid", "user_id": "uuid", "name": "indie coffee shops", "profile_type": "local_business", "fit_score": 82, "job_titles": ["owner", "general manager"], "industries": ["food and beverage"], "pain_points": ["slow weekday mornings"], "buying_signals": ["hiring baristas"], "discovery_keywords": ["specialty coffee"], "recommended_channels": [], "created_at": "2026-07-09T14:30:00Z", "updated_at": "2026-07-09T14:30:00Z" } } ``` **errors:** - 400 SERVER_002: campaign is completed or deleted - 404 SERVER_003: audience not found - 404 CAMP_001: campaign not found #### POST /audiences/generate AI-generate library profiles from a product (scrape + analysis). returns the created profiles; generated names already in your library are skipped, not overwritten. rate limited to 5 per hour. **scope:** `generate` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | product_name | string | yes | product name (1-200 chars) | | product_url | https url | no | product website - scraped for extra context | | product_description | string | no | what it does and who it's for (max 2000 chars) | **response (201):** ```json { "success": true, "data": [ { "id": "uuid", "name": "boutique fitness studios", "description": "independent gyms and studios booking classes online", "profile_type": "local_business", "fit_score": 88, "job_titles": ["owner", "studio manager"], "industries": ["fitness", "wellness"], "company_sizes": ["1-10"], "pain_points": ["no-shows", "manual scheduling"], "buying_signals": ["hiring front desk staff"], "discovery_sources": ["google_maps", "instagram"], "discovery_keywords": ["boutique fitness", "yoga studio"], "outreach_angle": "cut no-shows with automated scheduling", "contacts_discovered": 0, "created_at": "2026-07-09T14:30:00Z", "updated_at": "2026-07-09T14:30:00Z" } ] } ``` **errors:** - 403 BILLING_003: audience generation needs an active subscription - 422 SERVER_005: validation failed - 429 RATE_001: generation rate limit exceeded (5 per hour) ### contacts manage contacts and import leads. #### GET /contacts list all contacts **scope:** `read` **query parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | page_size | integer | no | items per page (default: 50) | | search | string | no | search by name or email | **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "email": "jane@example.com", "first_name": "Jane", "last_name": "Smith", "company_name": "Acme Inc", "tags": ["lead", "enterprise"] } ], "pagination": { "page": 1, "page_size": 50, "total": 1, "total_pages": 1 } } ``` #### POST /contacts create a single contact. idempotent on email: if a contact with the same email already exists for this user, returns 201 with the existing record (merging any new fields you supply) instead of 409. **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | email | string | yes | contact email | | first_name | string | no | first name | | last_name | string | no | last name | | company_name | string | no | company name | | tags | string[] | no | tags for categorization | | campaign_id | uuid | no | campaign to associate with | **errors:** - 429 CONTACT_009: contact limit reached for billing period #### GET /contacts/:id get a specific contact **scope:** `read` **errors:** - 404 CONTACT_001: contact not found #### PATCH /contacts/:id update a contact **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | first_name | string | no | first name | | last_name | string | no | last name | | company_name | string | no | company name | | tags | string[] | no | tags | **errors:** - 404 CONTACT_001: contact not found #### DELETE /contacts/:id soft-delete a contact (sets deleted_at timestamp) **scope:** `write` **response:** 204 no content **errors:** - 404 CONTACT_001: contact not found #### POST /contacts/bulk bulk import contacts (up to 500 per request) **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | contacts | array | yes | array of contact objects (email required, first_name/last_name/company_name optional) | | campaign_id | uuid | no | campaign to associate contacts with | **response (200):** ```json { "success": true, "data": { "created": 46, "updated": 2, "failed": 2, "contacts": [ { "id": "uuid", "user_id": "uuid", "email": "jane@example.com", "first_name": "Jane", "last_name": "Smith", "company_name": "Acme Inc", "job_title": null, "phone": null, "website": null, "tags": [], "enrichment_data": null, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z", "deleted_at": null } ], "errors": [{ "email": "bad@", "error": "invalid email format" }] } } ``` **errors:** - 429 CONTACT_009: contact limit reached for billing period **notes:** - duplicates are merged by email (counted as updated). max 500 contacts per request. #### DELETE /contacts/bulk bulk soft-delete contacts by ID **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | contact_ids | uuid[] | yes | non-empty array of contact IDs to delete | **response (200):** ```json { "success": true, "data": { "deletedCount": 12, "affectedCampaignIds": ["uuid"], "totalArchivedConversations": 3 } } ``` **notes:** - sets deleted_at on each contact - same soft-delete semantics as DELETE /contacts/:id. #### GET /contacts/usage get contact usage for the current billing period **scope:** `read` **response (200):** ```json { "success": true, "data": { "total_contacts": 1240, "period_contacts": 150, "period_limit": 30, "period_start": "2024-01-01T00:00:00Z", "period_end": "2024-02-01T00:00:00Z" } } ``` **notes:** - period_limit is per-campaign, not per-period. null on plans where the per-campaign cap is unlimited (admin/team plans). - period_contacts counts contacts created in the current billing period; total_contacts is the lifetime count of non-deleted contacts on the account. #### GET /contacts/:id/conversations get conversations for a specific contact **scope:** `read` **errors:** - 404 CONTACT_001: contact not found ### conversations manage conversations across all channels. #### GET /conversations list all conversations **scope:** `read` **query parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | page_size | integer | no | items per page (default: 50) | | status | string | no | filter by status (active, completed, failed, human_review) | | campaign_id | uuid | no | filter by campaign | #### GET /conversations/:id get a conversation with its contact and full message thread **scope:** `read` **response (200):** ```json { "success": true, "data": { "id": "uuid", "campaign_id": "uuid", "contact_id": "uuid", "user_id": "uuid", "channel": "email", "status": "active", "metadata": null, "contacts": { "id": "uuid", "full_name": "Jane Smith", "email": "jane@example.com", "company_name": "Acme Inc" }, "messages": [ { "id": "uuid", "conversation_id": "uuid", "direction": "outbound", "channel": "email", "content": "Hi Jane, ...", "status": "sent", "metadata": null, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } ], "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } } ``` **errors:** - 404 CONVERSATION_001: conversation not found #### GET /conversations/:id/messages get messages for a conversation **scope:** `read` **errors:** - 404 CONVERSATION_001: conversation not found #### POST /conversations/:id/read mark a conversation as read **scope:** `write` **response (200):** ```json { "success": true, "data": { "unread_count": 0, "contact_id": "uuid", "already_read": false } } ``` **errors:** - 404 CONVERSATION_001: conversation not found #### GET /conversations/engagement get engagement metrics across conversations **scope:** `read` **response (200):** ```json { "success": true, "data": { "total": 250, "unread": 18, "by_status": { "active": 42, "archived": 8, "closed": 200 }, "by_channel": { "email": 210, "twitter": 25, "instagram": 15 } } } ``` ### messages manage message approval workflow and sending. #### GET /messages/pending list messages pending approval **scope:** `read` **query parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | page_size | integer | no | items per page (default: 50) | | campaign_id | uuid | no | filter by campaign | #### POST /messages/:id/approve approve a pending message for sending **scope:** `send` **errors:** - 404 MESSAGE_005: message not found - 409 MESSAGE_004: message already approved #### POST /messages/:id/reject reject a pending message **scope:** `send` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | reason | string | no | rejection reason | **errors:** - 404 MESSAGE_005: message not found #### POST /messages/:id/retry retry a failed message **scope:** `send` **errors:** - 404 MESSAGE_005: message not found #### POST /messages/:id/feedback submit feedback to revise a message with AI **scope:** `send` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | feedback | string | yes | revision instructions (e.g. 'make it more casual and shorter') | **errors:** - 404 MESSAGE_005: message not found - 429 AI_004: AI rate limit exceeded #### POST /messages/bulk bulk approve or reject messages **scope:** `send` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | action | "approve" | "reject" | yes | action to perform | | message_ids | uuid[] | yes | message queue item IDs (1-100) | | reason | string | no | rejection reason (only used when action is reject) | **response (200):** ```json { "success": true, "data": { "affected_count": 5, "contact_ids": ["uuid", "uuid"], "skipped_count": 0, "status": "approved" } } ``` **notes:** - approve returns affected_count, contact_ids, skipped_count, and status; reject returns affected_count and contact_ids only. ### AI AI-powered content generation and analysis. #### POST /ai/generate generate AI content (email subject + body) **scope:** `generate` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | generation_type | string | yes | type of content to generate (e.g. initial_outreach, follow_up, reply) | | tone | string | yes | tone for the generated content (e.g. professional, casual, warm-direct) | | campaign_id | uuid | no | campaign to scope the generation to | | contact_id | uuid | no | target contact ID | | prompt | string | no | additional freeform instructions for the generation | | input_context | object | no | structured context, e.g. { include_cta: boolean } | **errors:** - 429 AI_004: AI rate limit exceeded - 500 AI_002: AI generation failed **notes:** - requires an API key with `generate` scope. agents do not call this directly. drafting runs as an internal QStash worker inside POST /campaigns. #### POST /ai/generate/campaign generate campaign-level outreach content (email sequence, templates) from an inline brief. content-first: pass the strategy directly, no campaign id required **scope:** `generate` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | campaign_name | string | yes | campaign name (used as the brief title) | | objective | string | yes | campaign objective (e.g. "book DJ gigs", "sell SaaS to VPs of Eng") | | target_audience | string | yes | target audience description | | key_benefits | string[] | yes | non-empty list of key benefits to emphasise | | tone | string | yes | tone (e.g. professional, casual, warm-direct) | **errors:** - 422 SERVER_005: missing or invalid request body - 429 AI_004: AI rate limit exceeded #### POST /ai/analyze analyze an inbound message (email or DM) for intent, sentiment, key points, and a suggested reply **scope:** `generate` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | conversation_id | uuid | yes | conversation ID: the analysis is scoped to this thread's history and contact | | message_content | string | yes | inbound message text to analyze. `email_content` is accepted as a legacy alias. | **response (200):** ```json { "success": true, "data": { "intent_type": "interested", "sentiment": "positive", "key_points": ["asked about pricing for 50 seats", "wants demo next week"], "suggested_response": "Hi {{first_name}}, ...", "requires_human_review": false, "extracted_data": { "seats": 50, "timing": "next week" } } } ``` **errors:** - 429 AI_004: AI rate limit exceeded **notes:** - requires an API key with `generate` scope. agents do not call this directly. response analysis runs as an internal step of the campaign workflow when an inbound reply arrives. #### GET /ai/usage get AI generation counts for the current billing period, broken down by generation_type. iris does not enforce a separate AI quota - generations are bounded implicitly by the campaign envelope, so no limit field is exposed. **scope:** `read` **response (200):** ```json { "success": true, "data": { "billing_period_start": "2026-04-30T01:00:04.245Z", "billing_period_end": "2026-05-30T01:00:04.245Z", "total_generations": 14, "total_tokens": 11680, "by_generation_type": { "draft_email": 8, "draft_dm": 4, "suggested_response": 2 } } } ``` ### discovery search for and discover new contacts. #### POST /discovery/search start an asynchronous contact discovery search across configured sources (Instagram, TikTok, and public business listings). returns a job_id - poll GET /discovery/jobs/:id for results **scope:** `generate` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | keywords | string[] | yes | non-empty array of keywords to search for (e.g. ['coffee shop', 'cafe']) | | location | string | no | location to search around (city, address, lat/lng string) | | radius_miles | number | no | search radius in miles (default: 25) | | max_results | integer | no | maximum results to return (1-500) (default: 50) | | sources | string[] | no | discovery sources to use (e.g. ['instagram', 'tiktok']). defaults to all available sources | | campaign_id | uuid | no | campaign to associate discovered contacts with | | profile_type | "local_business" | "content_creator" | "influencer" | "b2b_professional" | "ecommerce" | "saas_company" | "agency" | "freelancer" | "other" | no | target profile type, used to route the appropriate discovery source | | enrich_emails | boolean | no | attempt to enrich discovered contacts with email addresses (default: true) | **response (200):** ```json { "success": true, "data": { "job_id": "uuid" } } ``` **errors:** - 429 DISCOVERY_008: discovery quota exceeded - 429 DISCOVERY_006: discovery rate limited **notes:** - discovery is asynchronous - the response returns a job_id only. poll GET /discovery/jobs/:id until status is 'completed' to retrieve the discovered contacts. - to fan out a discovery run by campaign target profile, use POST /discovery/search/campaign (also async - poll GET /discovery/jobs/:id). - requires an API key with `generate` scope. agents do not call this directly. discovery runs as an internal step of POST /campaigns. #### POST /discovery/search/campaign run discovery search using campaign target profiles **scope:** `generate` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | campaign_id | uuid | yes | campaign ID | | selected_profile_ids | uuid[] | no | specific target profile IDs to use (defaults to all profiles on the campaign) | | additional_keywords | string[] | no | extra keywords to append to each profile's discovery query | | max_results | integer | no | maximum results to return (10-500) (default: 50) | | sources | string[] | no | discovery sources to use (e.g. ['instagram', 'tiktok']). defaults to all available sources | **response (200):** ```json { "success": true, "data": { "job_id": "uuid" } } ``` **errors:** - 404 CAMP_001: campaign not found - 429 DISCOVERY_008: discovery quota exceeded **notes:** - discovery is asynchronous - the response returns a job_id only. poll GET /discovery/jobs/:id until status is 'completed'. #### GET /discovery/jobs/:id get the status of a discovery job **scope:** `read` **response (200):** ```json { "success": true, "data": { "id": "uuid", "status": "completed", "progress_percent": 100, "progress_message": "discovery complete", "error": null, "started_at": "2024-01-15T10:30:00Z", "completed_at": "2024-01-15T10:35:00Z", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:35:00Z", "contact_count": 42, "contacts": [] } } ``` **errors:** - 404 DISCOVERY_002: discovery job not found #### GET /discovery/sources list available discovery data sources **scope:** `read` **response (200):** ```json { "success": true, "data": [ { "id": "instagram", "name": "Instagram", "available": true }, { "id": "tiktok", "name": "TikTok", "available": true } ] } ``` ### analytics campaign performance metrics and reporting. #### GET /analytics/overview get overall analytics overview across all campaigns **scope:** `read` **response (200):** ```json { "success": true, "data": { "campaigns": { "total": 12, "by_status": { "draft": 4, "active": 3, "completed": 5 } }, "contacts": { "total": 1250 }, "conversations": { "total": 450, "by_status": { "active": 120, "closed": 330 } }, "messages": { "total": 230, "by_status": { "approved": 200, "pending_approval": 30 } } } } ``` **notes:** - by_status keys are dynamic. they reflect whatever statuses exist in the user's data. Common values: campaigns (draft, active, paused, completed); conversations (active, archived, closed); messages (pending_approval, approved, rejected, sent, failed). #### GET /analytics/campaigns/:id get analytics for a specific campaign **scope:** `read` **response (200):** ```json { "success": true, "data": { "campaign": { "id": "uuid", "name": "Q1 outreach", "status": "active", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-16T08:00:00Z" }, "contacts": { "total": 150, "by_status": { "active": 120, "opted_out": 30 } }, "conversations": { "total": 42, "by_status": { "active": 12, "closed": 30 } }, "messages": { "total": 230, "by_status": { "sent": 200, "pending_approval": 30 } }, "delivery_status": { "sent": 42, "delivered": 40, "opened": 28, "clicked": 12 } } } ``` **errors:** - 404 CAMP_001: campaign not found #### GET /analytics/performance get performance metrics over time **scope:** `read` **query parameters:** | name | type | required | description | |------|------|----------|-------------| | days | integer | no | number of days to include in the time series (1-90) (default: 30) | | campaign_id | uuid | no | filter to a single campaign | #### GET /analytics/response-time get average response time metrics **scope:** `read` **query parameters:** | name | type | required | description | |------|------|----------|-------------| | campaign_id | uuid | no | filter by campaign | #### GET /analytics/contact-types get contact type distribution **scope:** `read` **query parameters:** | name | type | required | description | |------|------|----------|-------------| | campaign_id | uuid | no | filter to a single campaign | ### billing view billing plans and subscription information. #### GET /usage get your current plan, quota usage, and billing period in one call - the campaign envelope (campaigns per period with used/remaining) plus the per-campaign contact and message limits. `"unlimited"` replaces numeric limits on team plans. **scope:** `read` **response (200):** ```json { "success": true, "data": { "plan": "starter", "is_locked": false, "is_team": false, "billing_period": { "start": "2026-07-01T00:00:00.000Z", "end": "2026-07-31T23:59:59.999Z" }, "limits": { "campaigns_per_period": { "limit": 5, "used": 2, "remaining": 3 }, "contacts_per_campaign": { "limit": 30 }, "messages_per_campaign": { "limit": 240 } } } } ``` #### GET /billing/plans get available billing plans and their features **scope:** `read` **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "plan_name": "free", "display_name": "Free", "description": "get started at no cost", "monthly_price_cents": 0, "yearly_price_cents": 0, "campaigns_per_period": 1, "contacts_per_campaign": 30, "messages_per_campaign": 240, "is_active": true, "sort_order": 0, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" }, { "id": "uuid", "plan_name": "starter", "display_name": "Starter", "description": "for growing outreach", "monthly_price_cents": 1200, "yearly_price_cents": 12000, "campaigns_per_period": 5, "contacts_per_campaign": 30, "messages_per_campaign": 240, "is_active": true, "sort_order": 1, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ] } ``` ### autopilot configure the autonomous outreach loop - discovery top-ups and drafts on a standing campaign, review-only. #### GET /autopilot get autopilot status: whether it's configured, the current config, the pending-review count for the standing campaign, and the latest run's step results **scope:** `read` **response (200):** ```json { "success": true, "data": { "configured": true, "config": { "enabled": true, "campaign_id": "uuid", "max_new_contacts_per_run": 10, "max_pending_reviews": 25, "run_hour": 2, "timezone": "America/New_York", "paused": false, "pause_reason": null, "created_at": "2026-07-01T00:00:00Z", "updated_at": "2026-07-09T02:00:00Z" }, "pending_reviews": 4, "last_run": { "run_date": "2026-07-09", "steps": [ { "step": "refresh_insights", "status": "completed" }, { "step": "discover_contacts", "status": "completed" } ] } } } ``` #### PATCH /autopilot create or update the autopilot config. merge semantics: only the fields you send change; the first call creates the config with defaults for anything omitted. autopilot never sends - every drafted message waits in your approval queue. **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | enabled | boolean | no | turn the nightly loop on or off. enabling requires a campaign_id and clears any pause state | | campaign_id | uuid | no | the ONE standing campaign autopilot tends (must be yours and draft, scheduled, or active). null to unset | | max_new_contacts_per_run | integer (10-50) | no | discovery batch size per nightly run (default: 10) | | max_pending_reviews | integer (5-200) | no | skip discovery while this many drafts sit unreviewed in the approval queue (default: 25) | | run_hour | integer (0-23) | no | local hour the nightly run fires (default: 2) | | timezone | string | no | IANA timezone for run_hour (e.g. America/New_York) (default: America/New_York) | **response (200):** ```json { "success": true, "data": { "enabled": true, "campaign_id": "uuid", "max_new_contacts_per_run": 15, "max_pending_reviews": 25, "run_hour": 2, "timezone": "America/New_York", "paused": false, "pause_reason": null, "created_at": "2026-07-01T00:00:00Z", "updated_at": "2026-07-09T14:30:00Z" } } ``` **errors:** - 400 SERVER_002: invalid timezone, or enabled without a campaign_id - 403 BILLING_003: autopilot needs an active subscription #### POST /autopilot/run queue a manual autopilot run immediately. steps that already completed today are skipped, so this tops up the approval queue rather than double-discovering. rate limited to 2 per hour. **scope:** `generate` **response (202):** ```json { "success": true, "data": { "queued": true, "run_date": "2026-07-09" } } ``` **errors:** - 400 SERVER_002: autopilot is disabled or paused - 403 BILLING_003: autopilot needs an active subscription - 404 SERVER_003: autopilot is not set up yet - 429 RATE_001: manual run rate limit exceeded (2 per hour) ### channels manage connected accounts and multi-channel messaging. #### GET /channels/accounts list connected social accounts **scope:** `read` **response (200):** ```json { "success": true, "data": [ { "id": "uuid", "channel": "twitter", "platform_username": "@myaccount", "status": "active", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } ] } ``` #### GET /channels/availability check which channels are available for messaging **scope:** `read` **response (200):** ```json { "success": true, "data": [ { "channel": "email", "available": true, "connected": true }, { "channel": "twitter", "available": true, "connected": true }, { "channel": "instagram", "available": true, "connected": false } ] } ``` #### POST /channels/send send a message through a specific channel into an existing conversation **scope:** `send` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | channel | "email" | "twitter" | "instagram" | yes | outreach channel | | conversation_id | uuid | yes | ID of an existing conversation to send the message into | | recipient_id | uuid | yes | ID of the contact who will receive the message | | content | string | yes | message content (whitespace is trimmed) | | subject | string | no | subject line (email only - ignored for DM channels) | | variables | Record | no | key/value pairs substituted into the content template (e.g. {{first_name}}) | **errors:** - 404 CONVERSATION_001: conversation not found - 429 MESSAGE_015: message limit reached **notes:** - prerequisites: the conversation must already exist - there is no API endpoint to create one ad hoc. conversations are created by the campaign-activation flow when contacts are added to an active campaign. for queued AI drafts use the message-approval flow (GET /messages/pending → POST /messages/:id/approve). - prerequisites: twitter and instagram require an OAuth-connected account on the user. connect via the iris dashboard at /settings/channels - there is no API for connecting social accounts. - check GET /channels/availability before sending to avoid silent failures on disconnected channels. - requires an API key with `send` scope. agents do not call this directly. message sends run as an internal step of POST /campaigns + the message approval flow. ### notifications manage user notifications. #### GET /notifications list notifications **scope:** `read` **query parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | page_size | integer | no | items per page (default: 50) | | read | boolean | no | filter by read state (true/false) | | type | string | no | filter by notification type | #### PATCH /notifications/:id update a notification (e.g. mark as read) **scope:** `write` **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | read | boolean | yes | mark as read/unread | **errors:** - 404 NOTIFICATION_001: notification not found #### DELETE /notifications/:id delete a notification **scope:** `write` **response:** 204 no content **errors:** - 404 NOTIFICATION_001: notification not found #### POST /notifications/read-all mark all notifications as read **scope:** `write` **response (200):** ```json { "success": true, "data": { "updated_count": 12 } } ``` ### webhooks manage webhook endpoints for receiving real-time event notifications. all routes in this group require an API key with the `admin` scope (round-6 audit closed a privilege-escalation gap where a non-admin key could create or rotate webhooks). #### GET /webhooks list all webhook endpoints **query parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | page_size | integer | no | items per page (default: 50) | #### POST /webhooks create a new webhook endpoint **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | url | url | yes | webhook endpoint URL (HTTPS) | | events | string[] | yes | event types to subscribe to (e.g. ["campaign.created", "contact.created"]). use ["*"] for all events | | description | string | no | optional description | **response (200):** ```json { "success": true, "data": { "id": "uuid", "user_id": "uuid", "url": "https://example.com/webhooks/iris", "events": ["campaign.created", "contact.created"], "description": "production events", "active": true, "secret": "whsec_a3f5b9c2d8e1f4a7b6c9d2e5f8a1b4c7d0e3f6a9b2c5d8e1f4a7b0c3d6e9f2a5", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } } ``` **errors:** - 409 SERVER_016: a webhook is already registered for this URL. update or delete it before re-creating. **notes:** - the signing secret is only returned on creation - store it securely - new secrets are prefixed `whsec_`. legacy bare-hex secrets created before the prefix was introduced still verify normally. the HMAC is computed over the stored value as-is. - each (user, url) pair is unique. a 409 means you already have a webhook at that url. PATCH /webhooks/:id to update it, or DELETE /webhooks/:id then re-create. #### GET /webhooks/:id get a specific webhook endpoint **errors:** - 404 WEBHOOK_001: webhook not found #### PATCH /webhooks/:id update a webhook endpoint **request body (JSON):** | name | type | required | description | |------|------|----------|-------------| | url | url | no | webhook endpoint URL | | events | string[] | no | event types | | active | boolean | no | enable/disable | **errors:** - 404 WEBHOOK_001: webhook not found #### DELETE /webhooks/:id delete a webhook endpoint **response:** 204 no content **errors:** - 404 WEBHOOK_001: webhook not found #### POST /webhooks/:id/test send a test event to a webhook endpoint **response (200):** ```json { "success": true, "data": { "success": true, "delivery_id": "uuid", "response_status": 200, "response_body": "OK" } } ``` **errors:** - 404 WEBHOOK_001: webhook not found #### GET /webhooks/:id/deliveries list recent webhook deliveries **query parameters:** | name | type | required | description | |------|------|----------|-------------| | page | integer | no | page number (default: 1) | | page_size | integer | no | items per page (default: 50) | **errors:** - 404 WEBHOOK_001: webhook not found #### POST /webhooks/:id/rotate-secret rotate the signing secret for a webhook. the new secret is returned once and the old secret is invalidated immediately. update HMAC verification on the receiver side before the next delivery, otherwise signature checks will fail. **response (200):** ```json { "success": true, "data": { "id": "uuid", "secret": "whsec_b7c4d1e8f2a5c9d6e3f0a7b4c1d8e5f2a9b6c3d0e7f4a1b8c5d2e9f6a3b0c7d4" } } ``` **errors:** - 404 WEBHOOK_001: webhook not found ### agent credits credit-based payments for AI agents using x402 (USDC on Base) or MPP (Stripe). 1 credit = 1 campaign (30 contacts, 240 messages, AI drafting bundled in). no subscription required. #### GET /agent/credits check the agent's current credit balance **response (200):** ```json { "success": true, "data": { "balance": 7, "identity_id": "uuid" } } ``` **notes:** - requires x402 or MPP identity header (no payment needed for balance check). - returns null balance for API key users (they don't use credits). #### POST /agent/credits purchase 5 credits with x402 or MPP payment **response (201):** ```json { "success": true, "data": { "balance": 5, "credits_added": 5, "pack": "pack_5" } } ``` **errors:** - 402 PAYMENT_001: payment required - $12.00 (MPP) for 5 credits. x402 agents: use per-campaign pricing at POST /campaigns. - 500 PAYMENT_007: purchase failed after payment (contact support) **notes:** - credit packs: 5 credits for $12.00 (MPP only). x402 agents pay per-campaign ($2.25) at POST /campaigns. - x402 agents can pay per-campaign ($2.25) by replaying POST /campaigns with a PAYMENT-SIGNATURE header (x402 v2). MPP agents must use packs. - credits never expire. send PAYMENT-SIGNATURE (x402 v2) or Authorization: Payment (MPP) header. ## MCP server the iris MCP server lets AI agents manage campaigns, contacts, conversations, messages, and more. ```bash npx -y -p @jclvsh/iris iris-mcp ``` the package ships two binaries: `iris-mcp` (the MCP server, shown above) and `iris` (a CLI for terminal use). set the IRIS_API_KEY environment variable to your API key. recommended scopes: read, write, generate, send (or pass ["*"] for full access). ## OpenAPI spec OpenAPI 3.1 spec available at: https://api.iris-ai.dev/openapi.json ## error codes all errors return: `{ success: false, error: { code, message } }` ### authentication | code | HTTP | description | |------|------|-------------| | AUTH_001 | 401 | unauthorized - invalid or missing API key | | AUTH_002 | 403 | forbidden - insufficient permissions | | AUTH_003 | 401 | session expired | ### API keys | code | HTTP | description | |------|------|-------------| | APIKEY_001 | 401 | invalid or missing API key | | APIKEY_002 | 401 | API key expired | | APIKEY_003 | 401 | API key revoked | | APIKEY_004 | 403 | scope not granted for this operation | | APIKEY_005 | 429 | API key creation limit reached | | APIKEY_006 | 404 | API key not found | ### campaigns | code | HTTP | description | |------|------|-------------| | CAMP_001 | 404 | campaign not found | | CAMP_002 | 500 | campaign creation failed | | CAMP_003 | 500 | campaign update failed | | CAMP_005 | 409 | campaign already active | | CAMP_007 | 400 | campaign has no contacts | | CAMP_008 | 400 | invalid campaign status transition | | CAMP_009 | 429 | campaign limit reached for billing period | ### contacts | code | HTTP | description | |------|------|-------------| | CONTACT_001 | 404 | contact not found | | CONTACT_003 | 400 | invalid email address | | CONTACT_007 | 500 | bulk import failed | | CONTACT_009 | 429 | contact limit reached for billing period | ### conversations | code | HTTP | description | |------|------|-------------| | CONVERSATION_001 | 404 | conversation not found | | CONVERSATION_002 | 500 | conversation update failed | | CONVERSATION_003 | 500 | message send failed | ### messages | code | HTTP | description | |------|------|-------------| | MESSAGE_001 | 500 | message send failed | | MESSAGE_004 | 409 | message already approved | | MESSAGE_005 | 404 | message not found | | MESSAGE_015 | 429 | message limit reached for billing period | ### AI | code | HTTP | description | |------|------|-------------| | AI_001 | 503 | AI service unavailable | | AI_002 | 500 | AI generation failed | | AI_003 | 500 | AI analysis failed | | AI_004 | 429 | AI rate limit exceeded | | AI_005 | 400 | AI content filtered | ### discovery | code | HTTP | description | |------|------|-------------| | DISCOVERY_001 | 500 | discovery job failed | | DISCOVERY_002 | 404 | discovery job not found | | DISCOVERY_006 | 429 | discovery rate limited | | DISCOVERY_008 | 429 | discovery quota exceeded | ### webhooks | code | HTTP | description | |------|------|-------------| | HOOK_001 | 400 | invalid webhook signature | | HOOK_003 | 400 | invalid webhook payload | | WEBHOOK_001 | 404 | webhook not found | ### agent payments | code | HTTP | description | |------|------|-------------| | PAYMENT_001 | 402 | payment required: agent must send PAYMENT-SIGNATURE (x402 v2) or Authorization: Payment (MPP) header | | PAYMENT_002 | 402 | x402 payment verification failed | | PAYMENT_003 | 402 | MPP credential verification failed | | PAYMENT_004 | 500 | settlement failed after payment was charged. contact support | | PAYMENT_005 | 402 | insufficient credit balance: purchase a credit pack via POST /agent/credits | | PAYMENT_006 | 400 | invalid credit pack id | | PAYMENT_007 | 500 | credit purchase failed. if payment was charged, contact support | ### billing | code | HTTP | description | |------|------|-------------| | BILLING_004 | 402 | payment failed (Stripe charge declined) | | BILLING_010 | 429 | plan usage limit reached: upgrade to continue (alias of BILLING_USAGE_EXCEEDED) | | BILLING_011 | 402 | active subscription required for this operation | ### rate limiting | code | HTTP | description | |------|------|-------------| | RATE_001 | 429 | too many requests | | RATE_002 | 429 | API rate limit exceeded | ### server | code | HTTP | description | |------|------|-------------| | SERVER_001 | 500 | internal server error | | SERVER_002 | 400 | request validation failed (also returned for invalid Idempotency-Key) | | SERVER_003 | 404 | resource not found | | SERVER_004 | 405 | method not allowed | | SERVER_005 | 422 | invalid input (validation failed) | | SERVER_006 | 500 | database error | | SERVER_008 | 504 | request timeout | | SERVER_009 | 503 | service temporarily unavailable | | SERVER_016 | 409 | conflict: concurrent retry with same Idempotency-Key in flight |