list & filter contacts
GET /v1/contactsList contacts in your workspace, optionally filtered or searched. This is the endpoint behind “filter feedback by your top-mrr customers” — pair it with GET /v1/contacts/{contactId}/posts to see what a specific customer has filed.
query parameters
Section titled “query parameters”| param | type | default | notes |
|---|---|---|---|
q | string, 1-200 | — | substring search across email + name (case-insensitive ILIKE) |
sort | created | last_seen | mrr | created | newest first for created; most-recent-activity first for last_seen; mrr desc with NULLS LAST |
plan | string (csv) | — | exact-match filter; pass repeated or comma-separated (?plan=pro,enterprise) |
cursor | string | — | from the previous response’s nextCursor |
limit | integer, 1-100 | 25 | page size |
The plan filter is case-sensitive; pass the same string you used in identify.
migrating from other tools
Section titled “migrating from other tools”If you’re moving from another feedback tool’s customer identification, the field you need is externalUserId — your system’s stable user id, the same value you’d pass to spirby identify. The mapping:
| coming from | their field | becomes spirby |
|---|---|---|
| Canny | userID | externalUserId |
| Featurebase | customId | externalUserId |
| Segment | userId | externalUserId |
| PostHog | distinct_id | externalUserId |
| Intercom | user_id | externalUserId |
| Hubspot | contact vid (or your CRM key) | externalUserId |
Once externalUserId is set on a contact via identify, every subsequent identify call with the same id upserts the same contact — making this the migration anchor across systems.
curl examples
Section titled “curl examples”top mrr contacts on the pro plan
Section titled “top mrr contacts on the pro plan”curl -s "$SPIRBY_BASE/v1/contacts?sort=mrr&plan=pro&limit=20" \ -H "Authorization: Bearer $SPIRBY_KEY" | jqSort mrr returns highest mrr first; contacts with mrrCents = null are placed at the end (NULLS LAST), so a paginated walk eventually reaches them.
search by email or name
Section titled “search by email or name”curl -s --get "$SPIRBY_BASE/v1/contacts" \ --data-urlencode "q=acme" \ -H "Authorization: Bearer $SPIRBY_KEY" | jqILIKE substring match against email and name. “acme” finds “[email protected]” and “Acme Corp”.
most recently active
Section titled “most recently active”curl -s "$SPIRBY_BASE/v1/contacts?sort=last_seen&limit=10" \ -H "Authorization: Bearer $SPIRBY_KEY" | jqUseful for “what did our 10 most recently-seen customers ask for last week?” — pipe each result’s id into GET /v1/contacts/{contactId}/posts.
cursor pagination
Section titled “cursor pagination”# page 1curl -s "$SPIRBY_BASE/v1/contacts?sort=mrr&limit=25" \ -H "Authorization: Bearer $SPIRBY_KEY" | jq
# page 2 — pass the previous response's nextCursorcurl -s "$SPIRBY_BASE/v1/contacts?sort=mrr&limit=25&cursor=eyJpZCI6Im..." \ -H "Authorization: Bearer $SPIRBY_KEY" | jqCursors are stable across the sort dimension. Don’t try to decode them — the format is internal and may change.
response shape
Section titled “response shape”{ "data": [ { "id": "ctc_01H8...", "externalUserId": "usr_42", "name": "Acme Corp", "plan": "pro", "mrrCents": 14900, "currency": "USD", "metadata": { "signupSource": "google-ads" }, "firstSeenAt": "2026-05-14T16:32:00.000Z", "lastSeenAt": "2026-05-14T16:32:00.000Z", "createdAt": "2026-05-14T16:32:00.000Z", "updatedAt": "2026-05-14T16:32:00.000Z" } ], "nextCursor": "eyJpZCI6Im..."}nextCursor is null on the last page.
listing posts for one contact
Section titled “listing posts for one contact”The companion endpoint:
GET /v1/contacts/{contactId}/postsReturns every post attached to the contact across all boards, newest first. See the api reference for parameters.
Required api key scope: read.
related
Section titled “related”- per-contact detail + counts —
GET /v1/contacts/{contactId}. - upsert workflow — identify endpoint.
- the mcp equivalent —
list_contacts(snake_case fields).