KeelStack API
A read-only REST API over your sponsor pipeline, invoices, performance reports, and account activity. Pull KeelStack data into spreadsheets, Zapier, Make, n8n, your data warehouse, or any script.
Quickstart
- Sign in and open Settings → API keys (Business plan).
- Click Create key, name it after the integration you're building, and copy the key. It starts with
ks_and is shown exactly once — store it in your password manager or secrets store immediately. - Call any endpoint below with the key as a Bearer token.
curl https://keelstack.me/api/v1/me \
-H "Authorization: Bearer ks_YOUR_KEY"{
"data": {
"id": "usr_1a2b3c4d5e",
"email": "you@yournewsletter.com",
"name": "Alex Rivera",
"brand_name": "The Weekly Deploy",
"default_currency": "USD",
"plan": "business",
"plan_renews_at": "2026-10-01T00:00:00.000Z",
"account_created_at": "2026-06-14T09:30:00.000Z",
"docs": "/docs/api"
}
}Authentication
Every request carries your key in the Authorization header as a Bearer token:
Authorization: Bearer ks_YOUR_KEY- Keys are scoped to your entire account — they see every sponsor, invoice, report, and activity event you can see.
- Keys are read-only. The API cannot create, update, or delete anything. There is no write-method surface in v1 by design: a leaked key can never corrupt your CRM.
- Your private sponsor notes are never exposed over the API — the
notesfield exists only in the dashboard. - Revoke a key from Settings → API keys at any time; revocation is immediate.
- Treat keys like passwords. If one leaks, revoke it and create a new one — integrations using it stop at once.
Errors
Errors use a consistent JSON shape: { "error": string, "code"?: string }.
| Field | Description |
|---|---|
| 401 Unauthorized | Missing, malformed, revoked, or invalid key. |
| 402 Payment Required | The subscription behind this key lapsed (plan downgraded, cancelled and past period end, or expired). The error message says which and points at resubscribing. |
| 404 Not Found | The resource does not exist in this account. Ids are tenant-scoped — another account’s id 404s rather than leaking. |
| 422 Validation Failed | Malformed request body (only the key-management endpoints accept bodies). |
| 429 Too Many Requests | Rate limit exceeded. Includes a Retry-After header. |
| 5xx Server Error | Something failed on our side. Safe to retry with backoff. |
Rate limits
60 requests per minute per key. Every response includes these headers so well-behaved integrations can pace themselves:
| Field | Description |
|---|---|
| X-RateLimit-Limit | Requests allowed per window (60). |
| X-RateLimit-Remaining | Requests left in the current window. |
| X-RateLimit-Reset | Unix seconds when the window resets. |
| Retry-After | Seconds to wait — present on 429 responses only. |
Pagination
List endpoints use cursor pagination — stable under concurrent inserts, unlike page numbers.
| Field | Description |
|---|---|
| limit | Rows per page, 1–100. Default 25. |
| cursor | The id from the previous response’s pagination.next_cursor. |
| pagination.has_more | True when another page exists. |
| pagination.next_cursor | Pass as ?cursor= on the next call. Null on the last page. |
# Page through every sponsor, 50 at a time
curl "https://keelstack.me/api/v1/sponsors?limit=50" -H "Authorization: Bearer ks_YOUR_KEY"
# → { data: [...], pagination: { has_more: true, next_cursor: "spn_9f8e7d6c" } }
curl "https://keelstack.me/api/v1/sponsors?limit=50&cursor=spn_9f8e7d6c" -H "Authorization: Bearer ks_YOUR_KEY"Endpoints
GET/api/v1/me
Identity and plan state for the authenticated key — the natural first call for any integration. Use it to verify the key works and to fail fast with a clear message when the subscription has lapsed.
| Field | Description |
|---|---|
| data.id / data.email | Account identity. |
| data.brand_name | Brand shown on sponsor portals. |
| data.default_currency | Account-level display currency. Per-record amounts still carry their own currency. |
| data.plan / data.plan_renews_at | Billing state — renews_at is null during a trial. |
GET/api/v1/sponsors
List sponsors, newest first. Filter with ?stage=pitched|confirmed|active|renewed|churned.
Amounts are reported exactly as stored: each sponsor carries its own currency — never sum deal_value across currencies without grouping.
| Field | Description |
|---|---|
| data[].company / contact_name / contact_email | Core sponsor identity. |
| data[].stage | Pipeline stage. |
| data[].deal_value / currency | Deal size as entered, in the sponsor’s currency. |
| data[].contract_start / contract_end | Contract window (ISO dates or null). |
| data[].placement | What was agreed, e.g. “Aug newsletter + Ep. 47 podcast”. |
| data[].portal_message | The message you show this sponsor on their portal. |
| data[].reminder_cadence | Days-before-renewal reminder schedule, e.g. “30,14,7”. |
Deliberately excluded: notes (private) and portal-token internals (capability secrets).
GET/api/v1/sponsors/:id
One sponsor plus everything attached to it: invoices, performance reports, and asset metadata. Asset ids are metadata only — v1 has no file-download endpoint; fetch files from the dashboard.
| Field | Description |
|---|---|
| data.invoices[] | This sponsor’s invoices: number, amount, currency, status, issued/due/paid dates. |
| data.reports[] | Performance reports: period, opens, clicks, conversions, subscribers. |
| data.assets[] | Uploaded asset metadata: file_name, file_type, size_bytes, created_at. |
GET/api/v1/invoices
List invoices, newest issued first. Filters: ?status=draft|sent|paid|overdue|void and ?sponsor_id=….
| Field | Description |
|---|---|
| data[].number | Human-readable invoice number, e.g. KS-2026-0007. |
| data[].amount / currency | Invoice total in the invoice’s own currency. |
| data[].status | draft | sent | paid | overdue | void. |
| data[].dodo_invoice_url | Hosted payment link when the invoice was paid through Dodo. |
Deliberately excluded: free-text invoice notes (can contain banking details).
GET/api/v1/reports
List performance reports, newest first. Filter with ?sponsor_id=…. Use it to mirror campaign results into a sponsor-facing dashboard or warehouse.
GET/api/v1/audit-events
Your account's activity stream — the same events the sponsor Activity tabs render. Useful for syncing CRM activity into Slack, Notion, or a data pipeline.
| Field | Description |
|---|---|
| ?actor= | creator | sponsor | system | admin. |
| ?type= | An event type, e.g. asset.uploaded, portal.submitted, invoice.created, stage.changed. |
| ?sponsor_id= | Events for one sponsor. |
| data[].meta | JSON object with event details (returned parsed). |
Recipes
Post each new sponsor to Slack (Zapier)
- Trigger: Zapier “Schedule by Zapier” every 15 minutes (Zapier has no native KeelStack trigger).
- Action 1: “Webhooks by Zapier” GET to
https://keelstack.me/api/v1/sponsors?limit=100with your Bearer header. - Action 2: Filter
created_atnewer than the last run, then post each to Slack.
Weekly revenue snapshot (any runtime)
const BASE = 'https://keelstack.me/api/v1'
const key = process.env.KEELSTACK_API_KEY
const headers = { Authorization: `Bearer ${key}` }
async function* list(path) {
let cursor = null
do {
const url = new URL(BASE + path)
url.searchParams.set('limit', '100')
if (cursor) url.searchParams.set('cursor', cursor)
const res = await fetch(url, { headers })
if (res.status === 429) {
await new Promise(r => setTimeout(r, 1000 * Number(res.headers.get('Retry-After') ?? 5)))
continue
}
if (!res.ok) throw new Error(`${path} failed: ${res.status} ${(await res.json()).error}`)
const body = await res.json()
yield* body.data
cursor = body.pagination?.next_cursor ?? null
} while (cursor)
}
let paidThisMonth = 0
for (const inv of list('/invoices?status=paid')) {
if (new Date(inv.paid_at).getUTCMonth() === new Date().getUTCMonth()) {
// Group by currency — records can differ from the account default.
paidThisMonth += inv.amount // extend: bucket by inv.currency first
}
}
console.log('Paid invoices this month:', paidThisMonth)What the API deliberately does not do
- No writes. v1 is read-only end to end; the server rejects every non-GET request to /api/v1/* by construction. Write automation belongs in the dashboard.
- No private notes. The sponsor
notesfield is creator-private and never serialized by the API. - No file downloads. Assets are listed as metadata; download files from the sponsor portal or dashboard.
- No cross-tenant access. Every query is scoped to the key's own account; other accounts' ids simply 404.
Changelog
v1 — Initial release: /me, /sponsors, /sponsors/:id, /invoices, /reports, /audit-events. Bearer auth, cursor pagination, 60 req/min per key.
Questions or integration help?
Email hello@keelstack.me or use Support. Include the request path and status code — never paste your API key.