Features Solutions Pricing Integrations Resources About BNI Contact
Log in Start Free
Developer reference

ImpactiveAI CRM API

A simple HTTP + JSON API for syncing contacts, deals, invoices, projects and email sequences with ImpactiveAI CRM. Designed for Zapier, Make, n8n, custom backends, and AI agents like Claude Code or Cloud Co-Work.

Base URL

Sequence engagement filtering: sequence report fields uniqueOpens, uniqueClicks, totalOpens, totalClicks, openRate and clickRate, including contact drill-downs, use filtered activity. Recognisable bots, HEAD/prefetch probes, identifiable image-proxy opens and rapid sweeps of three distinct links within two seconds are excluded. These are estimates, not verified human engagement. Reports also expose rawUniqueOpens, rawUniqueClicks, rawTotalOpens and rawTotalClicks. Legacy per-email tracking counters remain raw. Filtering starts with requests recorded after the update; historical requests cannot be reliably reclassified. Rate denominators remain provider-accepted sends. Scanner requests still receive the normal image or link redirect.

All endpoints are served from your workspace host. If you use ImpactiveAI's standard domain, the base URL is:

https://app.impactiveai.com

If your workspace uses a custom domain (white-label setup), swap https://app.impactiveai.com for that domain — for example https://crm.yourcompany.com. Both the Agent API and Zapier API are reachable from any host that serves your workspace.

Authentication

Every request must include an x-api-key header. API keys look like icrm_ followed by 32 hex characters and are scoped to a single workspace.

x-api-key: icrm_8f3a1c2b4d5e6f7081928374a5b6c7d8

Create or revoke keys

In the app, open Workspace Settings → Integrations → API Keys. Click Create Key, give it a name, and copy the full key — it is only shown once. Revoke a key any time from the same screen; the change takes effect immediately on the next request.

Error responses

Agent API — /api/agent/*

A higher-level surface for AI agents and bespoke integrations. All endpoints require x-api-key, are restricted to that key's workspace, and never return mailbox credentials or OAuth tokens. Inbox actions are explicit. Delete moves mail to Trash, never permanently deletes it. Agents can create and edit reply drafts but cannot send them; sending requires a signed-in Hub user or your own mail client.

Inbox triage

POST/api/agent/inbox/batch

IMAP only. JSON body: {ids: string[], action: "archive"|"delete"|"done"}. Sequential, workspace-scoped per-account locking. Duplicate IDs run once. Returns {results:[{id,success,status,email?,error?,imapError?}],processed,total,failed}; failures do not stop later IDs. Archive moves to archiveFolder; delete moves to trashFolder, retaining the local row and appending “deleted to Trash”; done only changes local triageStatus. Failed moves do not update the email. Already moved rows succeed idempotently. Existing single-message agent /archive and /done are unchanged: /done still archives as well as completing.

POST/api/agent/inbox/:id/draft

IMAP inbound reply draft. Body: {bodyText,bodyHtml?,to?,cc?,subject?,pushToImap?:true}. To/Cc accept comma-separated addresses or arrays. Defaults to the sender, original Cc and Re: subject. bodyText is the reply content; the Hub rebuilds both MIME alternatives with blank-line-separated paragraphs, Calibri 11pt, and an Outlook-style inline quote of the original message and its quoted history. Any supplied bodyHtml is replaced by the generated formatting. The full account signature is not appended to replies. Adds original In-Reply-To and full References chain; APPENDs with \Draft. Returns 201 with the draft, including replyBodyText (unquoted editable text); original becomes drafted. One active draft per original: 409 if one already exists. A failed APPEND returns 502 plus imapError and draftId when a local draft was saved: edit that draft to retry, do not create another. No sending occurs.

GET/api/agent/drafts

Optional query status=draft,approved|sent|discarded (comma-separated), since (ISO, inclusive createdAt), emailId. Returns a draft array, newest updated first. GET /api/agent/drafts/:id returns one draft. Shape: id, workspaceId, emailAccountId, inReplyToEmailId, toAddresses, ccAddresses, subject, replyBodyText (editable reply only), bodyText (plain MIME part including quoted original), bodyHtml (formatted HTML MIME part including quoted original), status, createdBy, imapUid, imapFolder, sentEmailId, createdAt, updatedAt; also messageId, sendState, sendError, smtpAcceptedAt and inlineImages. No account credentials.

PATCH/api/agent/drafts/:id

Body: any edited bodyText/bodyHtml/to/cc/subject and optional status draft|approved|discarded. Supplying bodyText regenerates the paragraph-formatted HTML and plain-text reply, including the original quoted thread; bodyHtml is rebuilt rather than used as-is. Returns the updated draft. Edits APPEND the replacement first, then move the old draft to Trash (no EXPUNGE); discard moves the draft to Trash. Failed mailbox changes preserve the previous draft content. 409 for terminal/sending/uncertain drafts. No agent send route exists.

DELETE/api/agent/drafts/:id

Moves the corresponding IMAP Drafts message to Trash (never EXPUNGE), deletes the local draft row, and changes the original email's triageStatus from drafted back to triaged. Returns {id,deleted:true}. Only editable draft/approved rows can be deleted; IMAP failure leaves the local row intact.

Signed-in Email workflow

Open queue tabs exclude done/archived messages. Needs you also includes every drafted message, without changing its classification, so drafts from unclassified mail remain visible. On an unclassified message, choose a type before saving classification (the inline priority control prompts for it). Hub replies use paragraph-preserving Calibri 11pt formatting, quote the original inline, and do not append the account's full signature; SMTP/Gmail replies send the full References chain, while Microsoft uses Graph's native threading. Optional send body {expectedUpdatedAt: "ISO timestamp"} protects the reviewed draft version: a stale version returns 409 before SMTP. The Hub supplies it automatically. Repeated requests for a sent draft return alreadySent=true.

Browser-session routes, not API-key routes: PATCH /api/emails/:id/triage accepts partial {type,priority,summary,followUpAt:"YYYY-MM-DD"|null}, sets triagedBy=user and triagedAt, and changes new to triaged. Waiting defaults to three working days ahead. POST /api/emails/:id/reclassify clears classification and returns the email as new; agents cannot overwrite user classification, even with override=true. POST /api/emails/:id/action {action} returns {email}. POST /api/emails/:id/sender-trust {status:trusted|blocked,kind:address|domain} returns a manualOverride whitelist entry and refreshes matching senders. GET /api/emails/:id/activity returns the latest 100 audit records with action/source/actor/createdAt/result (JSON string).

Phase C task/support routes: POST /api/agent/inbox/:id/task (x-api-key) accepts optional {name,projectGid,sectionGid,dueOn:"YYYY-MM-DD"|null,notes,assigneeGid} (assigneeGid may be a workspace user GID or email) and returns {asanaTaskGid,asanaTaskUrl,projectGid,projectName,sectionGid,assigneeGid,assigneeName}; it returns 409 if a task is already linked and 422 if the selected project, section, or assignee is invalid. GET /api/agent/asana/projects?accountId=… returns the non-archived projects for that account's configured Asana workspace as an array of Asana project objects. GET /api/agent/asana/users?accountId=… returns workspace users ({gid,name,email}). Both require x-api-key and an account in the caller's workspace. POST /api/agent/inbox/:id/to-support returns {ticketId,ticketUrl}, or 409 if already linked. GET /api/agent/inbox/brief?accountId=…&date=YYYY-MM-DD returns {accountId,date,carriedOverCount,byType,draftsWaitingCount,tasksCreatedToday,openCount,priorityOne,waitingDue,headline}. Browser-session equivalents are POST /api/emails/:id/task (accepts projectGid, sectionGid and assigneeGid), POST /api/emails/:id/to-support, POST /api/emails/batch-create-tasks {ids} (uses account defaults), and GET /api/emails/triage-brief?accountId=…&date=YYYY-MM-DD.

POST /api/emails/batch-action has the same body and JSON response as the agent batch route. Optional Accept: application/x-ndjson streams {processed,total,result} after every ID and a final {complete:true,...results}. Browser draft equivalents: POST /api/emails/:id/draft, GET /api/drafts and /api/drafts/:id, PATCH /api/drafts/:id. POST /api/drafts/:id/send is session-only and sends exactly the stored reviewed draft; returns {draft,email?,warnings,alreadySent?}. SMTP acceptance is persisted before Sent APPEND (\Seen), stale-draft cleanup and original completion/archive. Post-send failures produce warnings, never a resend. Ambiguous SMTP failures become sendState=uncertain and block retries; verify your mail-client Sent folder. Incoming messages' isRead is untouched by these actions.

IMAP settings now include editable archiveFolder, draftsFolder and trashFolder. Trash discovery prefers \Trash, then Trash, then Deleted Items. MOVE requires native IMAP MOVE support; unsupported servers return 502 rather than using ImapFlow's destructive EXPUNGE fallback. Action tables: email_drafts and email_actions. Classification/action errors: 404 outside workspace, 409 conflicts, 422 validation/provider errors, 502 mailbox errors with imapError.

Hub classification runs (Phase D)

The Hub classifies only inbound new messages on IMAP accounts. User- and agent-classified rows are protected, including a conditional recheck at write time. Deterministic rules run first; remaining messages use one gpt-4o-mini JSON call each. Invalid or failed AI classifications leave the message new. Successful classifications set status triaged and triagedBy rule. Nothing is archived, moved, drafted, sent, marked read, or turned into a Support ticket.

POST/api/emails/triage-run

Browser-session route. JSON body { "accountId": "account-id" }. Returns 202 { "runId": "uuid", "status": "running" }. An active run returns its existing runId rather than creating a duplicate.

POST/api/agent/inbox/run-triage

Same start behavior, authenticated with x-api-key and scoped to that key's workspace. JSON body { "accountId": "account-id" }. Returns 202 with runId and status. Returns 404 for an account outside the workspace, 422 for invalid input or a non-IMAP account.

GET/api/emails/triage-run/:accountId

Browser-session run status. Returns only accountId, status (idle/running/complete/failed), and parsed progress. No credentials or OAuth tokens.

{ "accountId": "account-id", "status": "complete", "progress": { "runId": "uuid", "runKind": "manual", "processed": 20, "classified": 19, "total": 20, "errors": 1, "startedAt": "2026-10-10T06:00:00Z", "updatedAt": "2026-10-10T06:01:00Z", "finishedAt": "2026-10-10T06:01:00Z", "lastError": "AI returned invalid classification JSON", "llmCalls": 10, "llmTokens": 4200, "typeCounts": { "reply": 3, "noise": 16 } } }
GET/api/agent/inbox/run-status?accountId=account-id

API-key-authenticated status with the same response shape, workspace scoped. Returns 404 for an unknown/out-of-workspace account; 422 if accountId is absent.

IMAP account settings on the existing browser-session POST/PATCH email-accounts routes: triageAutoEnabled (default false), triageRunTimes (JSON string or array of 1–8 HH:MM times, default ["07:00","16:00"], Europe/London), triageOnSync (default false), triageRunCap (1–1000, default 300), triageLlmCallCap (0–1000, default 300). Scheduled runs are morning/afternoon; button and on-sync runs are manual. Interrupted runs with no heartbeat for ten minutes become failed; untouched messages remain new.

Account storage adds triageRunStatus and JSON-string triageRunProgress. Completed runs are recorded in email_triage_runs: llmCalls counts attempted AI calls, llmTokens records reported total token usage, typeCounts is JSON with counts of successfully classified types (including support/calendar). newCount is remaining new inbound mail; fyiCount includes calendar, while reply/task/noise/waiting retain existing aggregate fields. carriedOverCount counts inbound before today in Europe/London not done/archived. The classifier uses existing calendar subject/system hints and calendar text when headers are unavailable; it does not fetch IMAP headers.

Triage values: status new | triaged | drafted | tasked | archived | done; type reply | task | fyi | noise | waiting | calendar | support; priority 1 (highest) through 3. Messages classified as support are labeled only in this phase; they do not create a Support ticket.

The emails table stores nullable imapUid (the message UID in its synced IMAP folder) and repliedAt (the date of an outbound mail-client reply linked through In-Reply-To or References). These are sync metadata and are not added to the agent inbox JSON shapes. Incremental sync updates existing messages' isRead and fills a missing UID; it marks local INBOX rows done when no longer present in the recent server window, and links replies by message-id.

GET/api/agent/inbox

List inbound messages, newest first. Query parameters: accountId, comma-separated status (default new), type, priority, inclusive since (ISO timestamp), includeArchived (default false), inInbox=true (only rows confirmed by that account's most recent completed header refresh), limit (default 50, max 200), offset, and bodyChars (default 2000, max 50000). Each item includes headersRefreshedAt, which is null until refreshed, alongside message/thread IDs, dates, sender/recipient fields, truncated bodyText, read/automation/trust flags, CRM contact/company summaries, all triage fields, references header, and asanaTaskUrl. The inInbox filter requires a non-null refresh timestamp on or after the most recent completed refresh's startedAt; with no completed refresh, it returns no rows.

GET /api/agent/inbox?status=new,triaged&inInbox=true&limit=50&bodyChars=2000
POST/api/agent/inbox/refresh-headers

Refresh existing local messages from an IMAP account without fetching message bodies or attachments, changing the remote Seen flag, or writing local isRead. JSON body: accountId (required), days (integer 1–365, default 180). Walks INBOX and discovered Sent folders, matches by messageId, skips messages not held locally, and updates referencesHeader, isAutomated, hasListUnsubscribe, toMeDirect and senderTrust (blocked > trusted > contact > unknown). It does not insert, send, archive or delete messages.

Returns 202 with { "status": "running", "progress": "{...}" }; progress is a JSON string. Repeating the request while a run is active resumes it without resetting the cursor. Processes up to 500 UIDs per page, checkpoints each 50-message batch, and records totalInFolder. IMAP operations are serialized per account, connections close after each page, and there is a 2-second pause between pages. On errors it reconnects with 5s/15s/60s backoff, or 120s for connection-limit errors (up to 10 attempts); progress records connectionLimitRetries. Exhaustion sets status failed and saves lastError.

{ "accountId": "your-account-id", "days": 180 }
POST/api/agent/inbox/refresh-headers/cancel

Cancel a running header refresh. JSON body: {"accountId":"your-account-id"}. Returns {"status":"cancelled"}, or the account's current non-running status. Returns 404 for an account outside the workspace.

POST/api/agent/email-accounts/:id/backfill

API-key-authenticated version of the browser backfill route. Required JSON body: { "days": 180 } (integer 1–365). Background, resumable, paged 500 UIDs across INBOX and Sent, deduplicated by messageId. It imports missing messages only; use refresh-headers to refresh existing rows. IMAP operations are serialized per account, connections close after each page, and there is a 2-second pause between pages. Errors reconnect with 5s/15s/60s backoff, or 120s for connection-limit errors for up to 10 attempts; progress includes connectionLimitRetries and totalInFolder, and exhausted retries set status to failed.

Returns 202 with { "status": "running", "progress": "{...}" }, with progress as a JSON string. Returns 400 for invalid days, 404 for an unknown/out-of-workspace account, or 422 for a non-IMAP account. No browser session is required.

POST/api/agent/email-accounts/:id/backfill/cancel

Cancel a running backfill for the account in the path. No request body is required. Returns {"status":"cancelled"}, or the account's current non-running status.

GET/api/agent/email-accounts/:id/sync-status

Read only the sync/job status for an account in the API key's workspace. No query parameters or request body. Returns exactly the fields below, with both progress values parsed as objects; never returns credentials or OAuth tokens. Returns 404 for an unknown/out-of-workspace account. Job statuses include idle, running, complete, failed and cancelled. Progress includes folder totals, cursor, retryAttempt and an optional lastError; after a completed refresh, lastCompletedStartedAt preserves that run's start time even while a newer refresh is running.

{ "id": "your-account-id", "fromEmail": "you@example.com", "lastSyncAt": null, "backfillStatus": "idle", "backfillProgress": {}, "headerRefreshStatus": "running", "headerRefreshProgress": { "days": 180, "folders": ["INBOX", "Sent"], "fetched": 500, "updated": 480, "totalInFolder": 856, "folderIndex": 0, "cursorUid": 12345, "startedAt": "2026-10-08T12:00:00.000Z" } }
POST/api/agent/inbox/reconcile

Manually archive local inbound messages that are missing from INBOX after a completed header refresh. JSON body: accountId and optional positive-integer olderThanDays (default 14). Only rows with status new, emailDate older than the cutoff, and headersRefreshedAt null are changed. Sets status archived, triagedBy rule, and the reason Not in INBOX at reconcile; it makes no IMAP changes and is never scheduled automatically. Requires the account's header refresh status to be complete. Returns {"count":N}.

GET/api/agent/inbox/:id

Fetch one inbound message with full, untruncated bodyText, bodyHtml, its headersRefreshedAt, and a thread array. Thread messages are oldest first, include direction, and each includes headersRefreshedAt. Returns 404 when the message is not in the key's workspace.

GET/api/agent/inbox/carried-over

List inbound messages dated before 00:00 Europe/London today whose status is neither done nor archived. A waiting message with followUpAt is excluded. Supports account, type, priority, pagination, and body truncation filters; returns the same item shape as the inbox list.

PATCH/api/agent/inbox/:id/triage

Set triage fields. Required JSON: type, priority, summary, reason; optional status and date followUpAt. Sets triagedBy=agent and triagedAt. A message last changed by a user returns 409 even with override:true; use Reclassify in the Hub first. Invalid enum values return 422.

{"type":"task","priority":2,"summary":"Prepare revised quote","reason":"Customer requested pricing","status":"triaged","followUpAt":"2026-10-12","override":false}
POST/api/agent/inbox/triage

Apply the same triage fields to a JSON array of objects, each with an id. Returns a per-message result array; user-override conflicts are reported as 409 entries without stopping other updates.

[{"id":"EMAIL_ID","type":"fyi","priority":3,"summary":"Project update","reason":"No action requested"}]
POST/api/agent/inbox/:id/archive

Move an IMAP message to its account's configured archive folder, creating that folder if needed, then set local folder to Archive and status to archived. Optional JSON body: {"reason":"..."}. Idempotent when already archived. IMAP move failures return 502 with imapError; the local row is not changed on failure. Does not delete the message.

POST/api/agent/inbox/:id/done

Archive the IMAP message and set status to done. Optional JSON body: {"reason":"..."}. Uses the same 502 behavior as archive when the remote move fails.

GET/api/agent/inbox/summary

Optional date in YYYY-MM-DD (default today, Europe/London) and accountId. Returns byTriageStatus, byTriageType, carriedOverCount, confirmedInInbox (inbound rows confirmed by the latest completed refresh and not done or archived, across dates), and priorityOne items containing id, from, subject, and summary.

POST/api/agent/triage-runs

Insert an email triage run. JSON requires accountId and runKind (morning | afternoon | eod_snapshot | bankruptcy); count fields and note are optional. Count fields: newCount, repliedDraftedCount, taskedCount, fyiCount, noiseCount, waitingCount, and carriedOverCount. Returns the created row.

GET/api/agent/triage/whitelist

List whitelist entries for required accountId; optional status filters trusted, blocked, or neutral rows. Each row includes its email/domain kind, status, and manualOverride.

PATCH/api/agent/triage/whitelist/:id

Set JSON {"status":"trusted"} (or blocked/neutral). Marks the row manualOverride:true, so scheduled rebuilds preserve it, and recomputes sender trust only for open triage messages matching this address or domain.

POST/api/agent/triage/whitelist

Create or update a manual override. JSON: accountId, either email or domain, kind (address/domain), and status (trusted/blocked/neutral). Recomputes sender trust only for messages matching that address or domain.

GET/api/agent/contacts

List contacts in the workspace. Optional limit query parameter (default 100, max 500).

curl https://app.impactiveai.com/api/agent/contacts?limit=50 \ -H "x-api-key: icrm_..."
GET/api/agent/contacts/search

Find existing contacts before creating a new one. Provide email, phone, or both as query parameters. Email matching is case-insensitive and exact; phone matching compares digits only, ignoring spaces and punctuation. When both are supplied, a match on either value is returned. Results are limited to the API key's workspace.

Returns { "contacts": [...], "count": N }; no matches returns an empty array and count 0. At least one valid lookup value is required.

curl "https://app.impactiveai.com/api/agent/contacts/search?email=ada%40example.com&phone=%2B44%2020%207946%200000" \ -H "x-api-key: icrm_..."
POST/api/agent/contacts

Create one or many contacts. Send a single object, or an array of objects for batch create.

curl -X POST https://app.impactiveai.com/api/agent/contacts \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "firstName": "Ada", "lastName": "Lovelace", "email": "ada@example.com", "phone": "+44 20 7946 0000", "title": "Founder", "companyId": null, "tags": ["inbound","newsletter"] }'

Required: firstName, lastName. Returns the created contact (or an array of contacts for batch).

PATCH/api/agent/contacts/:id

Update a contact. Send only the fields you want to change.

curl -X PATCH https://app.impactiveai.com/api/agent/contacts/CONTACT_ID \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "title": "CEO", "tags": ["customer"] }'
POST/api/agent/contacts/:id/notes

Add a note activity to a contact in the API key's workspace. JSON body: {"body":"Caller asked about the renewal date."}. The body is required (1–10000 characters). Returns the created activity with entityType="contact" and type="note"; returns 404 if the contact is not in this workspace.

curl -X POST https://app.impactiveai.com/api/agent/contacts/CONTACT_ID/notes \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "body": "Caller asked about the renewal date." }'
GET/api/agent/contacts/:id/replies

List inbound emails received from this contact (up to 100, most recent first). Useful for agents that want to read replies before deciding next steps.

curl https://app.impactiveai.com/api/agent/contacts/CONTACT_ID/replies \ -H "x-api-key: icrm_..."
GET/api/agent/sending-accounts

List the SMTP sending accounts configured for this workspace. Use the returned id as sendingAccountId when creating or updating a sequence, so the agent can pick which "from" address the sequence sends from.

Each row contains: id, name, fromName, fromEmail, smtpHost, syncEnabled. Credentials are never returned. If the workspace has no SMTP accounts the list is empty and sequences fall back to the platform default sender (Resend).

curl https://app.impactiveai.com/api/agent/sending-accounts \ -H "x-api-key: icrm_..."
POST/api/agent/sequences

Create a new email sequence with optional inline steps. Each step needs a subject, body, and a delayDays (0 for the first send). status defaults to active. sendingAccountId is optional — call GET /api/agent/sending-accounts to pick one, or omit (or set to null) to fall back to the workspace default sender (Resend if no SMTP account is connected).

curl -X POST https://app.impactiveai.com/api/agent/sequences \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "name": "New lead nurture", "description": "Welcome flow for inbound demo requests", "status": "active", "sendingAccountId": null, "steps": [ { "subject": "Welcome", "body": "Hi {{firstName}}...", "delayDays": 0 }, { "subject": "Following up", "body": "Just checking in...", "delayDays": 3 } ] }'

Returns 201 with the created sequence and its steps array.

GET/api/agent/sequences

List every sequence in the workspace. Each row includes stepCount and an enrollmentCounts object (total, plus a count per status — active / paused / completed / unsubscribed).

curl https://app.impactiveai.com/api/agent/sequences -H "x-api-key: icrm_..."
GET/api/agent/sequences/:id

Fetch a single sequence with its full step list (ordered by position) and enrollment counts.

curl https://app.impactiveai.com/api/agent/sequences/SEQUENCE_ID \ -H "x-api-key: icrm_..."
PATCH/api/agent/sequences/:id

Update a sequence's name, description, status (draft / active / paused / archived), or sendingAccountId (must be an id returned by GET /api/agent/sending-accounts; set to null to send via the platform default).

curl -X PATCH https://app.impactiveai.com/api/agent/sequences/SEQUENCE_ID \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "status": "paused" }'
POST/api/agent/sequences/:id/steps

Append a new step (or insert at a specific zero-based position; existing steps at or after that index shift down). delayDays is the wait before this step is sent relative to the previous send.

curl -X POST https://app.impactiveai.com/api/agent/sequences/SEQUENCE_ID/steps \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "subject": "Quick check-in", "body": "Hi {{first_name}}, just following up...", "delayDays": 5 }'
PATCH/api/agent/sequences/:id/steps/:stepId

Edit any field on a step. Setting position reorders the sequence so positions stay contiguous.

curl -X PATCH https://app.impactiveai.com/api/agent/sequences/SEQUENCE_ID/steps/STEP_ID \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "subject": "New subject line", "delayDays": 2 }'
DELETE/api/agent/sequences/:id/steps/:stepId

Delete a step and compact the remaining positions back to 0..n-1.

curl -X DELETE https://app.impactiveai.com/api/agent/sequences/SEQUENCE_ID/steps/STEP_ID \ -H "x-api-key: icrm_..."
POST/api/agent/sequences/:id/enroll

Enroll contacts into a sequence by any combination of contactIds, tagIds, and listIds (at least one must be provided). The resolved audience is deduped against people already enrolled; skipped rows include a reason (already_enrolled or not_found).

Using tagIds or listIds requires the Audience module to be enabled for the workspace; plain contactIds always works.

curl -X POST https://app.impactiveai.com/api/agent/sequences/SEQUENCE_ID/enroll \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "contactIds": ["CONTACT_ID_1"], "tagIds": ["TAG_ID_NEWSLETTER"], "listIds": ["LIST_ID_VIP"] }'

Returns 201 with:

{ "enrolled": [ /* newly created enrollment objects */ ], "skipped": [ { "contactId": "...", "reason": "already_enrolled" } ], "resolvedCount": 12 }
GET/api/agent/sequences/:id/enrollments

List who is on the sequence plus their state. Each row contains: id, contactId, a denormalised contact summary (id, firstName, lastName, email), status (active / paused / completed / unsubscribed), currentStep, startedAt, nextSendAt, completedAt, pausedAt, pauseReason, lastSendError, lastSendErrorAt, consecutiveFailures, and updatedAt.

Supports ?since=ISO_8601 to only return enrollments updated strictly after that timestamp (exclusive boundary).

curl "https://app.impactiveai.com/api/agent/sequences/SEQUENCE_ID/enrollments?since=2026-05-20T00:00:00Z" \ -H "x-api-key: icrm_..."
GET/api/agent/sequences/:id/sends

List outbound emails sent by this sequence (up to 500, newest first). Each row contains: id, contactId, sequenceStepId, sequenceEnrollmentId, subject, fromAddress, toAddresses, body, emailDate. Sends remain visible here even after a contact is unenrolled (sequenceEnrollmentId may then be null). Sends that happened before this linkage was introduced are not returned.

Supports ?since=ISO_8601 filtered on emailDate >= since (inclusive boundary).

curl "https://app.impactiveai.com/api/agent/sequences/SEQUENCE_ID/sends?since=2026-05-20T00:00:00Z" \ -H "x-api-key: icrm_..."
GET/api/agent/sequences/:id/replies

List inbound replies from contacts currently enrolled in this sequence (up to 500, newest first). Each row contains: id, contactId, enrollmentId, subject, fromAddress, body, emailDate. Replies from contacts who have been fully unenrolled are not returned by this endpoint — use GET /api/agent/contacts/:id/replies if you need reply history per contact.

Supports ?since=ISO_8601 filtered on emailDate >= since (inclusive boundary).

curl "https://app.impactiveai.com/api/agent/sequences/SEQUENCE_ID/replies?since=2026-05-20T00:00:00Z" \ -H "x-api-key: icrm_..."
POST/api/agent/sequences/:id/pause

Pause active enrollments. Pass contactId to pause a single person, or contactIds for several. To pause everyone currently active on the sequence you must explicitly pass { "all": true } — an empty body pauses nothing and returns a 400, so a sequence is never paused by accident. Returns { "paused": <count> }.

curl -X POST https://app.impactiveai.com/api/agent/sequences/SEQUENCE_ID/pause \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "contactId": "CONTACT_ID" }'
POST/api/agent/sequences/:id/resume

Resume a single paused enrollment. Pass contactId; the enrollment is set back to active and continues from its current step and next send time — it is never restarted from the first email. Returns { "resumed": 1, "enrollment": {...} } when reactivated, { "resumed": 0 } if the enrollment was not paused (already active/completed), and 404 if the contact is not enrolled in the sequence.

curl -X POST https://app.impactiveai.com/api/agent/sequences/SEQUENCE_ID/resume \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "contactId": "CONTACT_ID" }'
DELETE/api/agent/sequences/:id/contacts/:contactId

Unenroll a contact from a sequence.

curl -X DELETE https://app.impactiveai.com/api/agent/sequences/SEQUENCE_ID/contacts/CONTACT_ID \ -H "x-api-key: icrm_..."
Polling tip: For the /enrollments, /sends and /replies endpoints, remember the highest updatedAt (enrollments) or emailDate (sends/replies) you've seen and pass it back as ?since= on the next poll to get only new rows. Note the boundary is exclusive for enrollments (>) and inclusive for sends/replies (>=), so for sends/replies de-dupe on the row id when the cursor matches the previous poll's max emailDate. A 5–15 minute poll interval is usually plenty.
GET/api/agent/bookings

List all bookings in the workspace.

curl https://app.impactiveai.com/api/agent/bookings -H "x-api-key: icrm_..."
POST/api/agent/bookings

Create a booking, optionally linked to an existing contact.

curl -X POST https://app.impactiveai.com/api/agent/bookings \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Discovery call — Ada Lovelace", "email": "ada@example.com", "scheduledAt": "2026-06-12T14:00:00Z", "contactId": "CONTACT_ID", "notes": "Interested in the pro plan" }'

Zapier / polling API — /api/zapier/*

Designed for poll-based integrations (Zapier, Make, n8n). List endpoints return up to 100 records sorted by updatedAt descending and support a ?since=ISO_8601 filter for incremental polling. Action endpoints (POST) create new records on demand.

GET/api/zapier/auth/test

Verify an API key. Returns the workspace's id and name. Used by Zapier's Auth Test step.

curl https://app.impactiveai.com/api/zapier/auth/test -H "x-api-key: icrm_..."
GET/api/zapier/companies

List companies. Optional ?since=2026-05-01T00:00:00Z returns only companies updated after that timestamp.

curl "https://app.impactiveai.com/api/zapier/companies?since=2026-05-01T00:00:00Z" \ -H "x-api-key: icrm_..."
POST/api/zapier/companies

Create a company. name is required.

curl -X POST https://app.impactiveai.com/api/zapier/companies \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Co", "website": "https://acme.example", "industry": "Software", "city": "London", "country": "United Kingdom" }'
GET/api/zapier/contacts

List contacts with optional ?since= filter. Each row includes companyName for convenience.

curl https://app.impactiveai.com/api/zapier/contacts -H "x-api-key: icrm_..."
POST/api/zapier/contacts

Create a contact. firstName and lastName are required.

curl -X POST https://app.impactiveai.com/api/zapier/contacts \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "firstName": "Grace", "lastName": "Hopper", "email": "grace@example.com", "companyId": "COMPANY_ID" }'
POST/api/zapier/lists

Create a static contact list. name is required; description is optional. These endpoints require the Audience module to be enabled and are scoped to the workspace belonging to the supplied API key. Names are matched case-insensitively within the workspace: a new list returns 201 with created: true; an existing list is returned with 200 and created: false.

Response: { "list": { "id", "workspaceId", "name", "description", "listType", "createdBy", "createdAt", "updatedAt" }, "created": true }.

curl -X POST https://app.impactiveai.com/api/zapier/lists \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Product updates", "description": "Contacts who asked for product news" }'
POST/api/zapier/lists/:id/members

Add contacts to an existing list without removing its current members. Pass either contactId or contactIds (up to 500 IDs). Repeating an addition is safe; existing memberships are counted as skipped. Every list and contact must belong to the API key's workspace.

Response: { "listId": "LIST_ID", "added": 1, "skipped": 0, "addedContactIds": ["CONTACT_ID"] }.

curl -X POST https://app.impactiveai.com/api/zapier/lists/LIST_ID/members \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "contactIds": ["CONTACT_ID", "ANOTHER_CONTACT_ID"] }'
POST/api/zapier/tags

Create a workspace tag. name is required; optional color is a display color. Names are matched case-insensitively within the workspace: a new tag returns 201 with created: true; an existing tag is returned with 200 and created: false.

Response: { "tag": { "id", "workspaceId", "name", "color", "createdBy", "createdAt" }, "created": true }.

curl -X POST https://app.impactiveai.com/api/zapier/tags \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Newsletter", "color": "#4f46e5" }'
POST/api/zapier/contacts/:contactId/tags

Add an existing workspace tag to a contact without replacing any other tags. Pass tagId. Repeating the same assignment returns added: false and does not create another membership.

Response: { "contactId": "CONTACT_ID", "tag": { "id", "name", "color" }, "added": true }. A new assignment returns 201; an existing assignment returns 200.

curl -X POST https://app.impactiveai.com/api/zapier/contacts/CONTACT_ID/tags \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "tagId": "TAG_ID" }'
GET/api/zapier/deals

List deals with stage name, company name, and primary contact name resolved.

curl "https://app.impactiveai.com/api/zapier/deals?since=2026-05-01T00:00:00Z" \ -H "x-api-key: icrm_..."
POST/api/zapier/deals

Create a deal. name is required; if stageId is omitted, the deal is placed in the first stage of the pipeline.

curl -X POST https://app.impactiveai.com/api/zapier/deals \ -H "x-api-key: icrm_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Acme — Q3 renewal", "value": 12000, "companyId": "COMPANY_ID", "contactId": "CONTACT_ID", "expectedClose": "2026-09-30", "source": "Referral" }'
GET/api/zapier/invoices

List invoices with totals, status, due/paid dates, and resolved company/contact names.

curl https://app.impactiveai.com/api/zapier/invoices -H "x-api-key: icrm_..."
GET/api/zapier/projects

List projects with stage name and company name.

curl https://app.impactiveai.com/api/zapier/projects -H "x-api-key: icrm_..."

Polling pattern for triggers

For poll-based integrations, store the highest updatedAt you've seen and pass it back as ?since= on the next poll. Records are returned newest-first, capped at 100 per request. Typical poll interval: 5–15 minutes.

Questions?

If you hit something unexpected, get in touch via the contact page. For Zapier-specific setup, see the Integrations page.

Ready to start?

Create a free workspace and generate your first API key in under a minute.

Create your free account