Docs API reference · updated September 20, 2026 · What the API is for
sumwerk API reference
Everything the dashboard shows can be pulled out, with the same numbers. With a read-and-write token you can also send what only your product knows (leads and trials that are not in Stripe, product usage, owners, tags, people, notes, tasks) and manage automations. sumwerk never writes to Stripe, whatever the token.
Base address https://www.sumwerk.com/api/v1. JSON in and out. Amounts are decimal strings in
major units ("49.00") with an explicit currency, dates and times ISO 8601, months in your
account’s time zone. The examples below are real calls against a demo account with invented
companies.
Authentication
Every call carries a token in the Authorization header:
$ curl https://www.sumwerk.com/api/v1/mrr \
-H "Authorization: Bearer swk_9f2c…"
HTTP/2 200
Getting a token. Account → API and exports → Create token (account owners). Give it a name that says where it will live (“billing backend”, “Zapier”), and pick its scope:
| Scope | Can |
|---|---|
| Read only | every GET |
| Read and write | also PUT, POST, PATCH, DELETE: customers, people, notes, tasks, automations |
The token (swk_…) is shown once. sumwerk stores only its SHA-256 digest, so nobody, us
included, can show it to you again: if it is lost, create a new one. A token belongs to one
account and sees only that account.
Sandboxes. An account can have several sandboxes (staging, one per
developer): further accounts with the same team and settings and their own data. A token created
in one starts with swk_test_, only sees that sandbox, and every answer carries
X-Sumwerk-Mode: sandbox and X-Sumwerk-Sandbox: <name> (or X-Sumwerk-Mode: live). Same URL, same calls:
use the swk_test_ token in staging and development.
Keeping it safe. Use it from a server, never from a browser or a mobile app. Give every system its own token, so that one can be revoked without stopping the others. Revoking (Account → API and exports → Revoke) works immediately. HTTPS only.
What goes wrong, and how it looks:
$ curl https://www.sumwerk.com/api/v1/mrr
HTTP/2 401
{ "error": "unauthorized" }
$ curl -X PUT https://www.sumwerk.com/api/v1/customers/org_7001 \
-H "Authorization: Bearer swk_readonly…" -H "Content-Type: application/json" \
-d '{"name":"Pine Robotics"}'
HTTP/2 403
{ "error": "forbidden",
"message": "This token is read-only. Create a read-and-write token on the account page." }
Limit: 600 requests per minute per token; beyond that 429 {"error":"rate_limited"}. Wait a
minute and go on: nothing was changed.
Reading
| Endpoint | Parameters | Returns |
|---|---|---|
GET /api/v1/mrr |
range = 12 (default), 24, 36, all |
currency, time_zone, close_after_days, months[]: month, closed (final: see below), closed_at, restatements (how often it changed after closing), mrr, arr, paying_customers, arpa, net_movement, fx_effect, movements{kind: {amount, customers}}, retention{}: customer_churn_rate, gross_mrr_churn_rate, net_mrr_churn_rate (fractions of what the month started with; the running month to date), net_revenue_retention_12m, gross_revenue_retention_12m (what the customers of twelve months earlier pay at this month end, of what they paid then); null when there is nothing to measure against |
GET /api/v1/restatements |
month = 2026-03 (optional) |
restatements[], newest first: id, month (null: all closed months at once, after a change of reporting currency or time zone), detected_at, reason, currency, mrr_before, mrr_after, mrr_difference, paying_customers_before, paying_customers_after, movements_before{}, movements_after{}, customers[] (customer_id, name, mrr_before, mrr_after; the 50 largest) |
GET /api/v1/reconciliation |
the latest self-check against Stripe’s invoices: checked_at, reconciled, compared, agreed, not_comparable{reason: count}, differences[] (subscription_id, invoice_id, customer_id, name, currency, sumwerk_monthly, stripe_invoice_monthly) |
|
GET /api/v1/risk-rules |
what “at risk” means for the account: unpaid_after_days, high_after_days, cancellation_severity, failed_card_severity, late_bank_transfer_severity, and signals[], your own rules: id, name, severity, conditions[] (field, op, value; the fields and operators of automations, listed as condition_fields and condition_ops), in_words. Writing: see below |
|
GET /api/v1/movements |
from, to (ISO dates, inclusive, account time zone), kind, customer (Stripe id) |
movements[]: date, occurred_at, kind, causes[], customer{stripe_id, name}, amount, currency, amount_reporting, mrr_before, mrr_after, sources[] |
GET /api/v1/customers |
status = paying, trial, lead or churned (paying=true still works) |
customers[]: id, external_id, external_ids[] (all your ids, the first one first), stripe_ids[], stripe_id (the first), name, email, status, mrr, arr, currency, mrr_reporting (in the account’s reporting currency, the sort order), customer_since, churned_at, trial_started_at, trial_ends_at, lead_created_at, owner, plan (the product of the largest subscription item, under your own plan names; who left keeps the last one), billing_interval (Monthly, Yearly, Every 6 months, …), tags[], custom{}, company, country, contacts[] |
GET /api/v1/customers/:id |
:id is our id, any of your own ids, or a Stripe customer id |
one customer |
GET /api/v1/segments |
by = owner (default), plan, billing_interval, tag, country, currency or custom.<attribute> |
attributes[] (your custom attributes with detected type: text, number, yes_no, date; filled, distinct), total{}, segments[]: segment, paying_customers, mrr, share, mrr_per_customer, churned_12_months, mrr_lost_12_months, churn_rate, decided_trials_12_months, converted, trial_to_paying. Numbers with many values come as ranges; the value most customers share (often 0) gets its own row |
GET /api/v1/attributes · PATCH /api/v1/attributes/:key |
your custom attributes: key, type (detected: text, number, yes_no, date), filled, distinct, label, description. An attribute is never created here: it appears when a value arrives (custom on a customer). PATCH sets label and description (write scope) |
|
GET /api/v1/lifecycle |
range = 12 (default), 24, 36, all |
in_trial_now, totals{}, months[] by the month the trial started: month, new_leads, trials_started, converted, still_in_trial, not_converted, conversion_rate (of the decided trials, 0 to 1), median_days_to_convert, paying_without_trial |
GET /api/v1/at-risk |
total, subscriptions[] (most severe first): severity (high | medium | low), reason (cancels | unpaid | past_due | signal: one of your own at-risk rules holds and nothing else flags the subscription), signals[] (names of your own rules that hold for the customer, also next to another reason), customer{stripe_id, name}, subscription_id, mrr, currency, mrr_reporting, overdue_days, unpaid_amount, ends_on, collection_method |
$ curl "https://www.sumwerk.com/api/v1/mrr?range=12" -H "Authorization: Bearer swk_9f2c…"
HTTP/2 200
{ "currency": "eur", "time_zone": "Europe/Berlin",
"months": [
…,
{ "month": "2026-09", "closed": false, "closed_at": null, "restatements": 0, "mrr": "36488.00", "arr": "437856.00", "paying_customers": 100,
"arpa": "364.88", "net_movement": "5747.00", "fx_effect": "0.00",
"movements": { "new": { "amount": "3274.00", "customers": 8 },
"expansion": { "amount": "2473.00", "customers": 7 },
"reactivation": { "amount": "0.00", "customers": 0 },
"contraction": { "amount": "0.00", "customers": 0 },
"churn": { "amount": "0.00", "customers": 0 } } } ] }
Closed months and restatements
A month closes close_after_days after it ends (5 unless changed under Account → General). closed: true
means the figures are final: copy them into your own reports without re-reading them every night. If late Stripe data, a
merge of two customers or a changed setting gives a closed month other figures, the month shows the new figures, its
restatements counter goes up, and GET /api/v1/restatements says when, why, from what to what, and which customers
were behind it. A nightly job that mirrors sumwerk needs only the open months plus the months named there.
The restatement webhook. The account’s owners get a mail when closed months were restated (unless they switch it off). A program cannot read mail, so Account → General also takes a web address of yours. Whenever closed months were restated, sumwerk sends one message there:
POST https://your-backend.example.com/webhooks/sumwerk
Content-Type: application/json
X-Sumwerk-Signature: t=1790000000,v1=5257a869e7ec…
{ "type": "month.restated", "occurred_at": "2026-10-14T06:07:31Z",
"data": { "restatements": [
{ "id": 12, "month": "2026-08", "detected_at": "2026-10-14T06:07:31Z",
"reason": "Customers merged: Fjord Labs into Fjord Labs AS", "currency": "eur",
"mrr_before": "30861.00", "mrr_after": "30741.00", "mrr_difference": "-120.00",
"customers_changed": 1 } ] } }
What it is for: your own reporting or data warehouse reads the named months again instead of
everything every night; or a “catch hook” address from Zapier or Make turns it into a message
in your finance channel. Check the signature as for automation webhooks (below), answer with
any 2xx. The address must be https:// and publicly reachable. Leave the field empty and
nothing is sent: the mail and GET /api/v1/restatements are enough for most teams.
Every movement names its cause and the Stripe references it was derived from:
$ curl "https://www.sumwerk.com/api/v1/movements?from=2026-09-01&kind=expansion" \
-H "Authorization: Bearer swk_9f2c…"
HTTP/2 200
{ "movements": [
{ "date": "2026-09-05", "occurred_at": "2026-09-05T14:00:00Z", "kind": "expansion",
"causes": ["price_change"],
"customer": { "id": 4373, "external_id": "org_1076", "stripe_id": "cus_Demo00076",
"name": "Harbor Cloud" },
"amount": "591.00", "currency": "eur", "amount_reporting": "591.00",
"mrr_before": "399.00", "mrr_after": "990.00",
"sources": ["in_demo1203", "evt_demo5203"] },
… ] }
A customer, by your own id (a Stripe customer id or our id work the same way):
$ curl https://www.sumwerk.com/api/v1/customers/org_1103 -H "Authorization: Bearer swk_9f2c…"
HTTP/2 200
{ "id": 4400, "external_id": "org_1103", "external_ids": ["org_1103"],
"stripe_ids": ["cus_Demo00103"], "stripe_id": "cus_Demo00103",
"name": "Lumen Energy", "email": "billing@lumenenergy.example", "company": "Lumen Energy",
"country": "NL", "status": "paying",
"mrr": "990.00", "arr": "11880.00", "currency": "eur", "mrr_reporting": "990.00",
"customer_since": "2026-09-03", "churned_at": null,
"trial_started_at": "2026-08-20", "trial_ends_at": "2026-09-03", "lead_created_at": "2026-08-20",
"owner": "jonas@nordwind.example", "tags": ["Sales led"],
"custom": { "plan": "Scale", "seats": 33, "company_type": "In-house", "active_users_30d": 18 },
"contacts": [ { "email": "anna@lumenenergy.example", "external_id": null, "first_name": "Clara",
"last_name": "Lindqvist", "title": "CTO", "phone": null, "linkedin": null } ] }
Revenue that still counts but is in doubt:
$ curl https://www.sumwerk.com/api/v1/at-risk -H "Authorization: Bearer swk_9f2c…"
HTTP/2 200
{ "reporting_currency": "eur", "total": "3369.00",
"subscriptions": [
{ "severity": "high", "reason": "cancels",
"customer": { "id": 4375, "external_id": "org_1078", "stripe_id": "cus_Demo00078",
"name": "Yarrow Media" },
"subscription_id": "sub_Demo4375", "mrr": "990.00", "currency": "eur",
"mrr_reporting": "990.00", "overdue_days": null, "unpaid_amount": "0.00",
"ends_on": "2026-10-01", "collection_method": "charge_automatically" },
… ] }
Writing: customers and contacts (token with write scope)
$ curl -X PUT https://www.sumwerk.com/api/v1/customers/org_7001 \
-H "Authorization: Bearer swk_9f2c…" -H "Content-Type: application/json" \
-d '{ "name": "Pine Robotics", "email": "hello@pinerobotics.example",
"trial_started_at": "2026-09-20", "trial_ends_at": "2026-10-04",
"owner_email": "mara@nordwind.example", "tags": ["Self serve"],
"custom": { "company_type": "Agency", "seats": 5 } }'
HTTP/2 201
{ "id": 4412, "external_id": "org_7001", "external_ids": ["org_7001"], "stripe_ids": [],
"name": "Pine Robotics", "email": "hello@pinerobotics.example", "status": "trial",
"mrr": "0.00", "arr": "0.00", "currency": null,
"trial_started_at": "2026-09-20", "trial_ends_at": "2026-10-04", "lead_created_at": "2026-09-20",
"owner": "mara@nordwind.example", "tags": ["Self serve"],
"custom": { "seats": 5, "company_type": "Agency" }, "contacts": [], … }
The same call again answers 200 and changes only what you send. Something that cannot be
right is refused as a whole, with the reason:
$ curl -X PUT https://www.sumwerk.com/api/v1/customers/org_7001 \
-H "Authorization: Bearer swk_9f2c…" -H "Content-Type: application/json" \
-d '{ "owner_email": "nobody@nordwind.example" }'
HTTP/2 422
{ "error": "invalid",
"message": "owner_email nobody@nordwind.example is not a user of this account" }
PUT /api/v1/customers/:your_id creates the customer or updates it (201 / 200). :your_id
is your own id for the customer, up to you (an org id, a UUID). Only the fields you send are
touched.
| Field | Meaning |
|---|---|
name, email, company, country |
text |
lead_created_at, trial_started_at, trial_ends_at |
ISO 8601 date or time. Status is derived: trial inside the window, paying with MRR, churned after, else lead |
also_known_as |
other ids of yours for the same customer (max 20). A customer that already exists under one of them is updated instead of a second one being created, and :your_id becomes one more of its ids: a lead you created from a booked call as jane@acme.example is found again when you later send the organisation as PUT /customers/<org id> with "also_known_as": ["jane@acme.example"]. Every id works in every URL afterwards. If the ids belong to two different customers the call is refused (422): merge them on the customer page |
owner_email |
a user of your sumwerk account; empty removes the owner |
tags |
list of strings, replaces the list (max 50) |
custom |
object of your own attributes: text, number or true/false. Merged into what is there; null removes one. Names: lowercase letters, digits, underscores (max 100) |
stripe_customer_ids |
Stripe customers that belong to this customer. If a record already mirrors that Stripe customer, it is adopted with its whole history instead of a second record being created. Adding a second one later links it; say why with link_reason: currency_change, resubscribed, duplicate_signup, separate_trial, billing_entity_change, other |
POST /api/v1/customers/:id/contacts upserts a person by email (first_name, last_name,
title, phone, linkedin), or by an external_id of yours for someone without an email
address (then a name is required); :id can be your id, ours or a Stripe id. Contacts come back on
GET /api/v1/customers/:id as contacts[].
Notes, calls and tasks
:id is our id, any of your ids, or a Stripe customer id, as everywhere.
| Endpoint | What it does |
|---|---|
GET /api/v1/customers/:id/notes |
notes[], newest first: id, external_id, kind (note | call), body (clean HTML), text (the same as plain text), call_duration (seconds), occurred_at, author_email, author_name, source |
POST /api/v1/customers/:id/notes |
writes a note or logs a call: body (plain text or HTML; only formatting and links survive), kind, call_duration, occurred_at (default now), author_email, author_name. With an external_id of yours the note is created once and updated after that (201 / 200), so a sync can send it again safely |
GET /api/v1/customers/:id/conversations |
support conversations mirrored from Plain, latest activity first: open (count), conversations[]: id, title, preview, status (todo | snoozed | done), priority, labels[], from, assignee, opened_at, last_activity_at, url |
GET /api/v1/customers/:id/tasks |
that customer’s tasks; open=true leaves out what is done |
GET /api/v1/tasks |
the account’s tasks by due day (max 1,000): open=true, assignee=<email>, due_before=<date> |
POST /api/v1/customers/:id/tasks |
adds a task: title, due_on (both required), assignee_email (a user of your account), external_id (created once, updated after that) |
PATCH /api/v1/tasks/:id |
:id is ours or your external_id. Change title, due_on, assignee_email, or tick it off with "completed": true (false opens it again) |
$ curl -X POST https://www.sumwerk.com/api/v1/customers/org_7001/notes \
-H "Authorization: Bearer swk_9f2c…" -H "Content-Type: application/json" \
-d '{ "external_id": "call_8842", "kind": "call", "call_duration": 1260,
"body": "Intro call. Wants to start with 5 seats.",
"author_email": "mara@nordwind.example" }'
HTTP/2 201
{ "id": 1974, "external_id": "call_8842", "kind": "call",
"body": "Intro call. Wants to start with 5 seats.",
"text": "Intro call. Wants to start with 5 seats.", "call_duration": 1260,
"occurred_at": "2026-09-20T13:22:05Z", "author_email": "mara@nordwind.example",
"author_name": "Mara Lind", "source": "api" }
$ curl -X POST https://www.sumwerk.com/api/v1/customers/org_7001/tasks \
-H "Authorization: Bearer swk_9f2c…" -H "Content-Type: application/json" \
-d '{ "title": "Send the proposal", "due_on": "2026-09-22",
"assignee_email": "mara@nordwind.example" }'
HTTP/2 201
{ "id": 5287, "external_id": null, "title": "Send the proposal", "due_on": "2026-09-22",
"completed": false, "completed_at": null, "assignee_email": "mara@nordwind.example",
"customer": { "id": 4412, "external_id": "org_7001", "name": "Pine Robotics" },
"source": "api" }
$ curl -X PATCH https://www.sumwerk.com/api/v1/tasks/5287 \
-H "Authorization: Bearer swk_9f2c…" -H "Content-Type: application/json" \
-d '{ "completed": true }'
HTTP/2 200
{ "id": 5287, "title": "Send the proposal", "completed": true,
"completed_at": "2026-09-20T13:22:05Z", … }
A task: id, external_id, title, due_on, completed, completed_at, assignee_email,
customer{id, external_id, name}, source. An automation that wants the team to act (“new
booking: reach out”) adds a task; nothing in sumwerk creates tasks by itself.
Events, automations and webhooks
Events are what happened to a customer, written once: customer.created,
customer.updated (data.changed[]: field names, custom attributes as custom.<name>),
trial.started, customer.became_paying, customer.churned, customer.reactivated,
cancellation.scheduled, cancellation.withdrawn, payment.failed, payment.recovered,
risk.signal_raised, risk.signal_cleared (a paying customer starts or stops matching one of
your own at-risk rules, Account → At-risk rules; data.signals[] names them),
support.conversation_opened, support.conversation_closed (with Plain connected)
(the last four carry data.subscription_id, the first of each pair also mrr, severity,
ends_on or overdue_days). API writes announce at once; what comes from Stripe is found by
the sync (hourly, or “Sync now”). Imports and the first sync of an account are history: their
events are recorded with their real date and set no automation in motion (older than 48 hours).
GET /api/v1/events?type=&customer=&after_id= lists them oldest first (500 per call): a tool
that cannot receive webhooks polls with the last id it has seen.
An automation says: when trigger happens and all conditions hold, do actions in order.
| Endpoint | |
|---|---|
GET /api/v1/automations |
the automations with how often each ran (runs{done, skipped, failed}), plus the lists of valid triggers, condition_fields, condition_ops, action_types |
POST /api/v1/automations · PATCH /api/v1/automations/:id · DELETE … |
create, change ("active": false switches it off), remove. An automation that cannot be read is refused with every problem named (422) |
GET /api/v1/automations/:id/runs |
the last 200 times the automation looked at an event: status (done | skipped | failed), and per action what it did, or the condition that was not met |
{ "name": "Owner and first tasks for agencies",
"trigger": "customer.updated", "trigger_fields": ["custom.company_type"],
"conditions": [ { "field": "owner", "op": "is_blank" },
{ "field": "custom.company_type", "op": "eq", "value": "Agency" } ],
"actions": [ { "type": "set_owner", "email": "tyan@yourcompany.com" },
{ "type": "add_task", "title": "Find on LinkedIn", "assignee": "owner",
"fallback_assignee": "james@yourcompany.com", "due_in_days": 3 },
{ "type": "webhook", "url": "https://hooks.zapier.com/hooks/catch/…" } ] }
trigger_fields(only withcustomer.updated): run only when one of these fields changed.- Conditions: fields
owner,source(api,stripe,manual,import),status,tags,name,email,company,country,currency,mrr(reporting currency, major units),external_id,has_stripe,changed,custom.<name>; opseq,not_eq,in,not_in,contains,is_blank,is_present,gt,lt,after,before. Text compares without regard to case. All must hold; an “or” is a second automation. - Actions (at most 10):
add_task(title,assignee:"owner"or an email address,fallback_assignee,due_in_days, default 2),set_owner(email),add_tags(tags),set_custom(key,value),send_email,enroll_in_sequence(see Email below),webhook(url, https only). An action that fails is logged and the others still run. What an automation changes never sets an automation in motion. - An automation looks at an event once. Automations run in a background job, seconds after the event.
$ curl -X POST https://www.sumwerk.com/api/v1/automations \
-H "Authorization: Bearer swk_9f2c…" -H "Content-Type: application/json" \
-d '{ "name": "Follow up on failed payments", "trigger": "payment.failed",
"conditions": [ { "field": "mrr", "op": "gt", "value": 300 } ],
"actions": [ { "type": "add_task", "title": "Call about the failed payment",
"assignee": "owner", "fallback_assignee": "mara@nordwind.example",
"due_in_days": 1 } ] }'
HTTP/2 201
{ "id": 12, "name": "Follow up on failed payments", "active": true,
"trigger": "payment.failed", "trigger_fields": [],
"conditions": [ { "field": "mrr", "op": "gt", "value": 300 } ],
"actions": [ { "type": "add_task", "title": "Call about the failed payment", "assignee": "owner",
"fallback_assignee": "mara@nordwind.example", "due_in_days": 1 } ],
"runs": { "done": 0, "skipped": 0, "failed": 0 }, "last_run_at": null }
$ curl https://www.sumwerk.com/api/v1/automations/9/runs -H "Authorization: Bearer swk_9f2c…"
HTTP/2 200
{ "runs": [
{ "id": 28, "at": "2026-09-20T13:21:36Z", "status": "done",
"results": [ { "ok": true, "type": "add_task",
"detail": "task 5284 for mara@nordwind.example, due 2026-09-21" } ],
"event": { "id": 326, "type": "cancellation.scheduled" },
"customer": { "id": 4315, "external_id": "org_1018", "name": "Iris Pay" } },
… ] }
$ curl "https://www.sumwerk.com/api/v1/events?type=cancellation.scheduled&after_id=300" \
-H "Authorization: Bearer swk_9f2c…"
HTTP/2 200
{ "events": [
{ "id": 325, "type": "cancellation.scheduled", "occurred_at": "2026-09-20T13:21:36Z",
"data": { "subscription_id": "sub_Demo4375", "reason": "cancels", "severity": "high",
"mrr": "990.00", "currency": "eur", "ends_on": "2026-10-01" },
"customer": { "id": 4375, "external_id": "org_1078", "name": "Yarrow Media" } },
… ] }
Webhooks are a POST of {id, type, occurred_at, data, automation{id, name}, customer{…}}
(the customer as GET /customers/:id returns it), 5 seconds timeout, no retry yet (a failure
shows in the run log). Verify before trusting: the header
X-Sumwerk-Signature: t=<unix time>,v1=<hex> is the HMAC-SHA256 of <t>.<raw body> with
your signing secret (Account → API and exports → Webhooks).
What your server receives, and how to check it (Ruby; any language with HMAC-SHA256 works the same way):
POST /hooks/sumwerk HTTP/1.1
Content-Type: application/json
User-Agent: sumwerk-webhooks/1
X-Sumwerk-Signature: t=1789910496,v1=5f0c…e91a
{ "id": 325, "type": "cancellation.scheduled", "occurred_at": "2026-09-20T13:21:36Z",
"data": { "subscription_id": "sub_Demo4375", "mrr": "990.00", "ends_on": "2026-10-01", … },
"automation": { "id": 9, "name": "Follow up on scheduled cancellations" },
"customer": { "id": 4375, "external_id": "org_1078", "name": "Yarrow Media", … } }
timestamp, signature = request.headers["X-Sumwerk-Signature"].match(/t=(\d+),v1=(\h+)/).captures
expected = OpenSSL::HMAC.hexdigest("SHA256", ENV["SUMWERK_WEBHOOK_SECRET"], "#{timestamp}.#{request.raw_post}")
head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(expected, signature) &&
Time.at(timestamp.to_i) > 5.minutes.ago
At-risk rules of your own
A paying customer whose attributes look like this is flagged as at risk, next to the payment
risks from Stripe (GET /api/v1/at-risk: reason: "signal", the rule’s name in signals[]).
At most 10 rules, each with up to 5 conditions that must all hold. A customer who does not
have the attribute never matches a comparison. Write scope.
| Call | Body | Answer |
|---|---|---|
POST /api/v1/risk-rules/signals |
name, severity (high | medium | low), conditions[] |
201 with the rule and its id |
PATCH /api/v1/risk-rules/signals/:id |
the same, complete | 200. Other conditions take stock again: who matches already sets no automation in motion |
DELETE /api/v1/risk-rules/signals/:id |
204 |
$ curl -X POST https://www.sumwerk.com/api/v1/risk-rules/signals \
-H "Authorization: Bearer swk_9f2c…" -H "Content-Type: application/json" \
-d '{ "name": "No project in 30 days", "severity": "medium",
"conditions": [ { "field": "custom.projects_created_30d", "op": "lt", "value": 1 } ] }'
HTTP/2 201
{ "id": "3fa9c1d2", "name": "No project in 30 days", "severity": "medium",
"conditions": [ { "field": "custom.projects_created_30d", "op": "lt", "value": 1 } ],
"in_words": "Projects created 30d is below 1" }
Operators besides the ones of automations’ conditions: gte, lte, and for dates
more_than_days_ago and within_last_days (value: a number of days), for example
{ "field": "custom.last_login_at", "op": "more_than_days_ago", "value": 14 }. A customer who
starts to match raises the event risk.signal_raised, one who stops risk.signal_cleared.
Email: templates, sending, sequences
| Endpoint | |
|---|---|
GET/POST /api/v1/email_templates, GET/PATCH/DELETE /api/v1/email_templates/:id |
name, subject, body (rich text). Merge fields (merge_fields[] in the list): {{ contact.first_name }}, {{ customer.name }}, {{ user.name }}, …, each with an optional fallback: {{ contact.first_name, fallback=there }}. An unknown field is refused |
POST /api/v1/customers/:id/emails |
sends one email: template_id, or subject + body; to = the address of one of the customer’s people (default: the first who may be emailed); from = the address of a user of your account (default: whoever created the token). Answers 201 with the email as it goes out (status: queued) |
GET /api/v1/customers/:id/emails |
what was written to this customer, newest first: to_email, from_email, subject, body, text, status (queued | sent | failed), via (google | sumwerk), error, origin (person | automation | sequence), sent_at |
GET/POST /api/v1/sequences, GET/PATCH/DELETE /api/v1/sequences/:id |
a sequence is sent and received whole: name, active, stop_when_paying, steps[]: subject, body, wait_days (days after the step before; ignored for the first) |
POST /api/v1/customers/:id/enrollments |
sequence_id, to, from as for emails. The first step goes out within ten minutes |
GET /api/v1/customers/:id/enrollments · DELETE /api/v1/enrollments/:id |
who is or was in which sequence (status: active | finished | stopped, stop_reason, next_step, next_run_at, emails_sent) · stop one |
Emails go out from the sender’s own Google mailbox when they connected it, otherwise through
sumwerk as “Name via sumwerk” with replies to them. A person marked do not email gets nothing
(422). Every email carries an unsubscribe link and the one-click unsubscribe headers (where the
text says {{ unsubscribe_url }}, otherwise a line at the end); unsubscribing marks the person
do not email and stops their sequences. Automations have two more actions: send_email (template_id, from: "owner" or a
user’s address, fallback_from) and enroll_in_sequence (sequence_id, from, fallback_from).
Linking Stripe customers changes MRR history; the recalculation runs a few seconds later, once
for a burst of writes. Invalid input answers 422 {"error":"invalid","message":"…"} and
changes nothing; a read-only token answers 403.
For AI assistants: MCP
The same data is available over the Model Context Protocol at https://www.sumwerk.com/mcp
(Streamable HTTP, the same bearer tokens and scopes). Tools: mrr_by_month, mrr_movements,
find_customers, get_customer, segments, funnel, revenue_at_risk, list_attributes,
list_tasks, and with a read-and-write token add_note and add_task. How to connect Claude:
Ask Claude about your customers.
Errors
An error is a status code and a JSON body with error, often with a message for a person.
Nothing is changed by a call that answers with an error.
| Status | error |
When |
|---|---|---|
400 |
invalid_parameter |
a date or number in the address cannot be read |
401 |
unauthorized |
no token, or one that is unknown or revoked |
403 |
forbidden |
a read-only token tried to write |
404 |
not_found |
no such customer, task or automation in your account |
422 |
invalid |
the body cannot be right; message names every problem |
429 |
rate_limited |
more than 600 requests in a minute |
503 |
fx_rates_missing |
an exchange rate for a reporting month is not in yet; try again later |
Reference lists
Kinds: new, expansion, reactivation, contraction, churn. Causes:
subscription_started, trial_conversion, resume, recovered, pause, delinquent,
cancellation, subscription_ended, price_change, quantity, discount_ended,
discount_applied. A source ending in :inferred means no invoice or event says when the
change happened; it is dated to the last evidence.
A customer can be looked up by our id, by your own external id or by any of its Stripe
customer ids. Movements and at-risk entries carry customer{id, external_id, stripe_id, name}.
CSV exports (signed in, from the Overview page)
| Export | URL | One row per |
|---|---|---|
| Months | /exports/months.csv?range=12\|24\|36\|all |
month: movements by kind + counts, net, FX effect, MRR, ARR, customers, ARPA, closed (yes / no), restatements, churn rates and revenue retention (fractions) |
| Movements | /exports/movements.csv |
movement, all time: date, customer, kind, causes, native + reporting amount, MRR before/after, Stripe sources |
| Segments | /exports/segments.csv?by=owner\|plan\|billing_interval\|tag\|country\|currency\|custom.<attribute> |
segment: paying customers, MRR, share, churn of the last 12 months, trial to paying |
| Funnel | /exports/lifecycle.csv?range=12\|24\|36\|all |
month a trial started: leads, trials, converted, still in trial, not converted, rate, median days |
| Customers | /exports/customers.csv (takes the list’s filters: status, q, owner, plan, billing_interval, tag, country, currency, attribute + value or min / max) |
customer: status, MRR, dates, owner, tags, ids, and one column per custom attribute (custom.<key>) |
| One month | /months/2026-09.csv (optional ?kind=churn) |
movement of that month |
| At risk | /at-risk.csv (optional ?severity=high\|medium\|low, ?reason=cancels\|unpaid\|past_due\|signal) |
subscription whose MRR still counts but is in doubt |