These two endpoints expose who in your community has opted out of your custom emails, so you can mirror those opt-outs into your own ESP suppression list, CRM, or warehouse.

EndpointPurpose
GET /public-api/v1/email-unsubscribesThe opt-out feed — who opted out of what, and when
GET /public-api/v1/email-subscription-typesYour community's subscription types — resolves the feed's subscriptionTypeId and supplies its valid filter values

Notes that apply to both endpoints on this page:

  • There are two kinds of opt-out. A specific opt-out is a member unsubscribing from one subscription type; an all opt-out is a member turning off every custom email at once. The feed returns both, and tells you which is which.
  • Opt-outs are read-only here. Members change their own preferences from the preference link in your emails, and you can manage subscription types from the dashboard.
  • Only custom-email opt-outs are covered. Per-channel notification preferences (event reminders, forum emails, digests) and bounce or spam-complaint records are not returned by these endpoints.

Get Email Unsubscribes

URL: https://api.gradual-api.com/public-api/v1/email-unsubscribes

HTTP Method: GET

Content type: application/json

Tier: Medium

Returns a paginated list of the custom-email opt-outs in your community, newest first.

Query Parameters:

All parameters are optional. Omit them all to get every opt-out in your community.

  • subscriptionTypeId, string, only return opt-outs from this one subscription type. Get the valid values from Get Email Subscription Types below.
  • scope, string, one of specific or all. Defaults to returning both.
  • email, string, only return opt-outs belonging to this member. Exact match, case-insensitive. Special characters must be URL-encoded — for example [email protected] must be sent as user%[email protected]. Unencoded + is decoded as a space by the URL parser and will be rejected with a 400 invalid email error.
  • unsubscribedAfter, string (ISO 8601), only return opt-outs recorded strictly after this moment, for example 2026-08-01T00:00:00Z. Use this to poll for new opt-outs instead of re-reading the whole list.
  • pageNum, integer (≥ 1), the page number to retrieve (1-based), defaults to 1
  • pageSize, integer (1–200), number of opt-outs per page, defaults to 50

Response Example:

{
"size": 2,
"pageNumber": 1,
"pageSize": 50,
"results": [
{
"userId": "6501f1a2b3c4d5e6f7a8b9c0",
"userEmail": "[email protected]",
"subscriptionTypeId": null,
"subscriptionTypeName": null,
"scope": "all",
"unsubscribedAt": "2026-08-03T09:12:44.000Z"
},
{
"userId": "6501f1a2b3c4d5e6f7a8b9c0",
"userEmail": "[email protected]",
"subscriptionTypeId": "6502f1a2b3c4d5e6f7a8b9c1",
"subscriptionTypeName": "Product Newsletter",
"scope": "specific",
"unsubscribedAt": "2026-08-01T10:00:00.000Z"
}
]
}

Response Fields:

  • size, number, the number of opt-outs returned in this page (i.e. results.length)
  • pageNumber, number, the page that was returned (echoes pageNum)
  • pageSize, number, the page size that was applied
  • results, array, the list of opt-out objects

Each item in results has these fields:

  • userId, string, the member's ID — the same value the Users endpoints return as userId
  • userEmail, string, the member's email address. null in the rare case where the member record carries no address.
  • subscriptionTypeId, string, the subscription type the member opted out of. null when scope is all.
  • subscriptionTypeName, string, the name of that subscription type, so you do not have to resolve the ID yourself. null when scope is all.
  • scope, string, specific for an opt-out from one subscription type, all for an account-wide opt-out from every custom email
  • unsubscribedAt, string (ISO 8601), when the opt-out was recorded. See the note on all rows below.

Notes:

  • One member can appear more than once. A member who opted out of two subscription types and then turned off all custom emails appears three times — once per specific row, plus one all row. This is deliberate: a suppression list needs to know which types an opt-out covers. Deduplicate on your side if you only care whether a member is suppressed at all.
  • There is no row ID. Identify a row by the pair userId + subscriptionTypeId (with subscriptionTypeId null for an all row).
  • Rows disappear when a member re-subscribes. This is a list of current opt-outs, not a change log, so there is no tombstone for a member who opts back in. If you keep your own copy, rebuild it from a full read periodically rather than relying on unsubscribedAfter alone.
  • Rows are sorted by unsubscribedAt, newest first.
  • Members who have been removed from or archived in your community are not included.
  • subscriptionTypeId also resolves subscription types you have since deleted from the dashboard, so historical opt-outs keep a readable subscriptionTypeName.
  • The response does not include a total count. To page through everything, keep incrementing pageNum until a page returns fewer than pageSize items (or an empty results array).
  • scope=all cannot be combined with subscriptionTypeId — an account-wide opt-out has no subscription type, so the combination is rejected with 400 rather than silently returning nothing. scope=specific with subscriptionTypeId is fine, as is subscriptionTypeId on its own (which only ever matches specific rows).
  • A subscriptionTypeId or email that does not exist in your community returns an empty results array, not a 404.
  • Each query parameter may be supplied at most once. A repeated parameter such as ?scope=specific&scope=all is rejected with 400.
  • Returns 400 if a parameter value is invalid (for example pageSize > 200, a subscriptionTypeId that is not a valid ID, or an unsubscribedAfter that is not a valid ISO 8601 timestamp).
  • pageNum × pageSize must not exceed 10,000, so at most 10,000 opt-outs are reachable in a single pass. Narrow the read with subscriptionTypeId, scope, or unsubscribedAfter if your community has more than that. Requests past the limit are rejected with 400.

A note on unsubscribedAt for all rows

For a specific row, unsubscribedAt is the moment the opt-out was recorded and never changes.

For an all row, it is the last time the member changed any of their notification preferences, because an account-wide opt-out carries no timestamp of its own. Two consequences:

  1. Polling with unsubscribedAfter re-delivers an all row you already have whenever that member edits an unrelated preference. Harmless if you apply rows as upserts, which we recommend anyway.
  2. Within a single paged read, an all row whose member changes a preference mid-read can move position between pages, and be missed. If you need an exact account-wide list, read it in one pass with ?scope=all and a large pageSize, or re-read it rather than resuming a stale page.

specific rows are not affected by either.

Example: build a suppression list

Pull every opt-out once, then poll for new ones.

# 1. First run: page through every opt-out in the community
curl -H "x-client-id: $CLIENT_ID" -H "authorization: Bearer $TOKEN" \
"https://api.gradual-api.com/public-api/v1/email-unsubscribes?pageSize=200&pageNum=1"
# 2. Keep incrementing pageNum until a page returns fewer than pageSize rows
# 3. Later runs: only what is new since the newest unsubscribedAt you stored
curl -H "x-client-id: $CLIENT_ID" -H "authorization: Bearer $TOKEN" \
"https://api.gradual-api.com/public-api/v1/email-unsubscribes?unsubscribedAfter=2026-08-03T09:12:44.000Z&pageSize=200"

To suppress only one newsletter rather than everything, read that type plus the account-wide opt-outs, and union the two:

# Everyone who opted out of this specific newsletter
curl -H "x-client-id: $CLIENT_ID" -H "authorization: Bearer $TOKEN" \
"https://api.gradual-api.com/public-api/v1/email-unsubscribes?subscriptionTypeId=6502f1a2b3c4d5e6f7a8b9c1&pageSize=200"
# Plus everyone who opted out of all custom emails
curl -H "x-client-id: $CLIENT_ID" -H "authorization: Bearer $TOKEN" \
"https://api.gradual-api.com/public-api/v1/email-unsubscribes?scope=all&pageSize=200"

Get Email Subscription Types

URL: https://api.gradual-api.com/public-api/v1/email-subscription-types

HTTP Method: GET

Content type: application/json

Tier: Medium

Returns a paginated list of your community's custom-email subscription types, in the same order the dashboard shows them.

Use this to build an ID → name map before reading the opt-out feed, and to find the subscriptionTypeId values you can filter that feed by. Unlike the feed, it also lists types that currently have no opt-outs at all.

Query Parameters:

All parameters are optional.

  • includeHidden, string, true or false, defaults to false. false returns only the types currently in use; true also returns retired types.
  • systemKey, string, only return the system-created type with this key, for example club_announcement. Exact match.
  • pageNum, integer (≥ 1), the page number to retrieve (1-based), defaults to 1
  • pageSize, integer (1–200), number of types per page, defaults to 50

Response Example:

{
"size": 2,
"pageNumber": 1,
"pageSize": 50,
"results": [
{
"id": "6502f1a2b3c4d5e6f7a8b9c1",
"name": "Product Newsletter",
"description": "Monthly product news and release notes",
"hidden": false,
"createdAt": "2026-03-14T08:00:00.000Z",
"updatedAt": "2026-07-02T11:20:00.000Z"
},
{
"id": "6502f1a2b3c4d5e6f7a8b9c2",
"name": "Club Announcements",
"description": null,
"hidden": false,
"createdAt": "2026-01-09T04:11:00.000Z",
"updatedAt": "2026-01-09T04:11:00.000Z"
}
]
}

Response Fields:

  • size, number, the number of types returned in this page (i.e. results.length)
  • pageNumber, number, the page that was returned (echoes pageNum)
  • pageSize, number, the page size that was applied
  • results, array, the list of subscription type objects

Each item in results has these fields:

  • id, string, the subscription type's ID. This is the value Get Email Unsubscribes reports as subscriptionTypeId and accepts as its subscriptionTypeId filter.
  • name, string, the type's name, as members see it
  • description, string, the description shown to members on the preference page. null when unset.
  • hidden, boolean, true when the type has been retired
  • createdAt, string (ISO 8601), when the type was created
  • updatedAt, string (ISO 8601), when the type was last changed

Notes:

  • Types are returned in the order they appear in your dashboard, not alphabetically or by date.
  • Names are editable, so do not use name as a join key. Match on id instead.
  • Deleting a subscription type in the dashboard retires it — it is marked hidden: true rather than erased, so opt-outs recorded against it are preserved. Those opt-outs still appear in the feed, so pass includeHidden=true here when you need to resolve every ID the feed can return.
  • includeHidden accepts only the literal strings true and false; 1 and 0 are rejected with 400.
  • An unknown systemKey, or a community with no subscription types at all, returns an empty results array, not a 404.
  • Each query parameter may be supplied at most once. A repeated parameter is rejected with 400.
  • Returns 400 if a parameter value is invalid, for example pageSize > 200.
  • Most communities have only a handful of types, so one page is normally enough.