Followkite API
A JSON REST API at https://followkite.com/api/v1. It does what the app does, with the same validation, plan limits, do-not-contact rules and activity log, so nothing you send through it can bypass the safety controls.
Authentication and keys
Create a key in Integrations → API keys. Send it as Authorization: Bearer fk_live_…. A key belongs to one workspace and acts with the role of the person who created it, narrowed to its scopes. If that person leaves the workspace, the key stops working. Browser cookies are never accepted on /api/v1.
| Scope | Allows |
|---|---|
contacts:read | Read contacts, lists and custom fields |
contacts:write | Create, update, delete and verify contacts; add them to lists |
campaigns:read | Read campaigns, their results and enrollments |
campaigns:write | Launch, pause and resume campaigns |
suppressions:read | Read the do-not-contact list |
suppressions:write | Add addresses and domains to it |
inbox:read | Read Unibox conversations |
reports:read | Read reports and deliverability |
send | Send transactional email and read its messages and templates |
Errors and limits
Errors are JSON: {"error": "invalid_input", "field": "…"}. Common codes: invalid_api_key (401), insufficient_scope (403, with required), ip_not_allowed (403), plan_limit_… (403), rate_limited (429). Each key may make 120 requests a minute; see the X-RateLimit-* headers.
Endpoints
| Method | Path | What it does | Scope |
|---|---|---|---|
GET | /me | The workspace and key in use | — |
GET | /contacts | List contacts. Query: q, list, status (active, suppressed, valid, risky, invalid, unverified), limit, offset | contacts:read |
POST | /contacts | Create a contact: email, firstName, lastName, company, title, customFields, listIds | contacts:write |
GET | /contacts/{id} | One contact with its recent emails | contacts:read |
PATCH | /contacts/{id} | Update a contact | contacts:write |
DELETE | /contacts/{id} | Delete a contact | contacts:write |
POST | /contacts/verify | Verify up to 200 contacts: contactIds, or none for the oldest unverified | contacts:write |
GET | /lists | Contact lists | contacts:read |
POST | /lists/{id}/members | Add contacts to a list: contactIds | contacts:write |
GET | /campaigns | Campaigns with counts | campaigns:read |
GET | /campaigns/{id}/stats | Results by email and A/B version | campaigns:read |
POST | /campaigns/{id}/pause | Pause a campaign (also launch, resume) | campaigns:write |
GET | /suppressions | The do-not-contact list | suppressions:read |
POST | /suppressions | Add addresses | suppressions:write |
GET | /unibox | Conversations. Query: view (leads, interested, all, unread, auto, bounces, archived) | inbox:read |
GET | /reports | Sending report | reports:read |
GET | /deliverability | Inbox health, domain DNS and blocklists, safety settings | reports:read |
POST | /send | Send one transactional email | send |
POST | /send/batch | Send to up to 1,000 recipients with personal data | send |
GET | /messages | Transactional activity. Query: status, to, tag, batch, offset | send |
GET | /messages/{id} | One message with its events | send |
POST | /messages/{id}/cancel | Cancel a scheduled or waiting message | send |
GET | /templates | Transactional templates | send |
const res = await fetch('https://followkite.com/api/v1/contacts', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.FOLLOWKITE_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'ann@clinic.com', firstName: 'Ann', company: 'Bright Smiles', listIds: ['…'] }),
});
if (res.status === 429) await new Promise((r) => setTimeout(r, Number(res.headers.get('retry-after')) * 1000));Transactional email
Send receipts, password resets and alerts from your app. Set up your sending domain under Transactional email → Settings (or connect your own Amazon SES or SMTP relay there), and send from a domain you have verified on the Domains page.
curl https://followkite.com/api/v1/send \
-H "Authorization: Bearer fk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-receipt" \
-d '{
"from": "Shop <orders@yourdomain.com>",
"to": "ann@example.com",
"template": "order-receipt",
"data": { "name": "Ann", "order": { "id": "1042" }, "items": [{ "title": "Cap", "qty": 2 }] },
"tags": ["receipt"],
"metadata": { "orderId": "1042" }
}'HTTP/1.1 202 Accepted
{ "id": "7c1e…", "status": "SENT", "to": "ann@example.com", "subject": "Receipt #1042",
"tags": ["receipt"], "metadata": { "orderId": "1042" }, "template": { "id": "…", "version": 3 }, "duplicate": false }Fields
from:"Name <address>"or{ email, name }; defaults to the workspace's default sender.to,replyTo: one address each.subject+htmland/ortext, ortemplate(slug or id) + optionalversionanddata. Plain text is generated from the HTML when missing.headers: up to 20X-headers (alsoList-Unsubscribe,List-Id).tags: up to 10.metadata: up to 20 keys; both are echoed in webhooks.sendAt: ISO time up to 30 days ahead; cancel withPOST /messages/{id}/cancel.sandbox: true: checks and renders without delivering.track: { opens, clicks }: optional tracking.
Batch: POST /send/batch takes the same fields plus recipients: [{ to, data, tags, metadata }] (up to 1,000). Each recipient is checked on its own; the response lists accepted and rejected rows. Batches go out from the queue within a minute; single sends are attempted at once.
Templates
Hi {{name|there}}, variable with a fallback
Order {{order.id}} nested fields
{{#if paid}}Paid{{else}}Due{{/if}} conditionals ({{#unless}} too)
{{#each items}}{{this.title}} × {{this.qty}}{{/each}} loops; {{@index}} is the position
{{{raw_html}}} not escaped (HTML only)Every save is a new version; restore an old one from the editor. Set text direction to right-to-left for Arabic or Urdu.
Statuses
SCHEDULED, QUEUED, SENT (accepted by your provider), DELIVERED (SES confirmed), DEFERRED (retrying), BOUNCED, COMPLAINED, DROPPED (address suppressed), FAILED, CANCELLED, SANDBOX. A send whose outcome is unknown is marked FAILED with DELIVERY_UNCERTAIN and never retried, so nobody gets two password resets.
Limits and safety
Your plan sets a monthly allowance. In a workspace's first week, transactional email is limited to 200 a day. Sending pauses automatically when bounces reach 5% or spam complaints 0.1% over 100 or more emails in 7 days. Hard bounces and complaints are added to the do-not-contact list.
Webhooks
Add an endpoint in Integrations. Each event is an HTTPS POST signed with X-Followkite-Signature: t=…,v1=… (HMAC-SHA256 of t.body with your signing secret). Failed deliveries are retried with backoff. Events: reply.received, lead.interested, contact.unsubscribed, contact.bounced, campaign.completed, and for transactional email email.sent, email.delivered, email.deferred, email.bounced, email.complained, email.dropped, email.failed, email.opened, email.clicked. Slack alerts for interested leads are set up separately in Integrations.
Questions
How do I authenticate with the Followkite API?
Create an API key under Integrations → API keys (workspace owners and admins). Send it on every request as Authorization: Bearer fk_live_…. Keys are shown once, stored only as a hash, limited to the scopes you choose, and can be limited to IP addresses and given an expiry date.
What are the API rate limits?
Each key can make 120 requests a minute. Every response has X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers; over the limit you get 429 with Retry-After.
How do I avoid sending a transactional email twice?
Send an Idempotency-Key header (or idempotencyKey in the body). A second request with the same key returns the first message with "duplicate": true and sends nothing.
Which providers deliver transactional email?
Followkite sending by default: add the three DNS records shown under Transactional email → Settings → Sending domains, and delivery, bounce and complaint events are included. You can instead use your own Amazon SES account, or any SMTP relay such as Postmark, Mailgun, Brevo or SendGrid. Connect one in Transactional email → Settings. With SES, delivery, bounce and complaint events come back through an SNS subscription; SMTP relays only report that they accepted the message.
Do campaign unsubscribes block transactional email?
No. Receipts and password resets are not marketing, so only addresses that hard-bounced or reported spam are skipped. Those messages get status DROPPED.
Last updated .