Skip to content

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.

Availability: the API is included with the Business plan. Create your first key in Dashboard → Settings → API keys. Trial accounts can use the API with the same limits while evaluating.

Quickstart

  1. Sign in and open Settings → API keys (Business plan).
  2. 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.
  3. 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 notes field 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 }.

FieldDescription
401 UnauthorizedMissing, malformed, revoked, or invalid key.
402 Payment RequiredThe 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 FoundThe resource does not exist in this account. Ids are tenant-scoped — another account’s id 404s rather than leaking.
422 Validation FailedMalformed request body (only the key-management endpoints accept bodies).
429 Too Many RequestsRate limit exceeded. Includes a Retry-After header.
5xx Server ErrorSomething 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:

FieldDescription
X-RateLimit-LimitRequests allowed per window (60).
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetUnix seconds when the window resets.
Retry-AfterSeconds to wait — present on 429 responses only.

Pagination

List endpoints use cursor pagination — stable under concurrent inserts, unlike page numbers.

FieldDescription
limitRows per page, 1–100. Default 25.
cursorThe id from the previous response’s pagination.next_cursor.
pagination.has_moreTrue when another page exists.
pagination.next_cursorPass 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.

FieldDescription
data.id / data.emailAccount identity.
data.brand_nameBrand shown on sponsor portals.
data.default_currencyAccount-level display currency. Per-record amounts still carry their own currency.
data.plan / data.plan_renews_atBilling 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.

FieldDescription
data[].company / contact_name / contact_emailCore sponsor identity.
data[].stagePipeline stage.
data[].deal_value / currencyDeal size as entered, in the sponsor’s currency.
data[].contract_start / contract_endContract window (ISO dates or null).
data[].placementWhat was agreed, e.g. “Aug newsletter + Ep. 47 podcast”.
data[].portal_messageThe message you show this sponsor on their portal.
data[].reminder_cadenceDays-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.

FieldDescription
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=….

FieldDescription
data[].numberHuman-readable invoice number, e.g. KS-2026-0007.
data[].amount / currencyInvoice total in the invoice’s own currency.
data[].statusdraft | sent | paid | overdue | void.
data[].dodo_invoice_urlHosted 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.

FieldDescription
?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[].metaJSON object with event details (returned parsed).

Recipes

Post each new sponsor to Slack (Zapier)

  1. Trigger: Zapier “Schedule by Zapier” every 15 minutes (Zapier has no native KeelStack trigger).
  2. Action 1: “Webhooks by Zapier” GET to https://keelstack.me/api/v1/sponsors?limit=100 with your Bearer header.
  3. Action 2: Filter created_at newer 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 notes field 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.