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.
| Endpoint | Purpose |
|---|---|
GET /public-api/v1/email-unsubscribes | The opt-out feed — who opted out of what, and when |
GET /public-api/v1/email-subscription-types | Your 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 ofspecificorall. 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 asuser%[email protected]. Unencoded+is decoded as a space by the URL parser and will be rejected with a400 invalid emailerror.unsubscribedAfter, string (ISO 8601), only return opt-outs recorded strictly after this moment, for example2026-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 to1pageSize, integer (1–200), number of opt-outs per page, defaults to50
Response Example:
{"size": 2,"pageNumber": 1,"pageSize": 50,"results": [{"userId": "6501f1a2b3c4d5e6f7a8b9c0","subscriptionTypeId": null,"subscriptionTypeName": null,"scope": "all","unsubscribedAt": "2026-08-03T09:12:44.000Z"},{"userId": "6501f1a2b3c4d5e6f7a8b9c0","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 (echoespageNum)pageSize, number, the page size that was appliedresults, 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 asuserIduserEmail, string, the member's email address.nullin the rare case where the member record carries no address.subscriptionTypeId, string, the subscription type the member opted out of.nullwhenscopeisall.subscriptionTypeName, string, the name of that subscription type, so you do not have to resolve the ID yourself.nullwhenscopeisall.scope, string,specificfor an opt-out from one subscription type,allfor an account-wide opt-out from every custom emailunsubscribedAt, string (ISO 8601), when the opt-out was recorded. See the note onallrows 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
specificrow, plus oneallrow. 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(withsubscriptionTypeIdnullfor anallrow). - 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
unsubscribedAfteralone. - Rows are sorted by
unsubscribedAt, newest first. - Members who have been removed from or archived in your community are not included.
subscriptionTypeIdalso resolves subscription types you have since deleted from the dashboard, so historical opt-outs keep a readablesubscriptionTypeName.- The response does not include a total count. To page through everything, keep incrementing
pageNumuntil a page returns fewer thanpageSizeitems (or an emptyresultsarray). scope=allcannot be combined withsubscriptionTypeId— an account-wide opt-out has no subscription type, so the combination is rejected with400rather than silently returning nothing.scope=specificwithsubscriptionTypeIdis fine, as issubscriptionTypeIdon its own (which only ever matchesspecificrows).- A
subscriptionTypeIdoremailthat does not exist in your community returns an emptyresultsarray, not a404. - Each query parameter may be supplied at most once. A repeated parameter such as
?scope=specific&scope=allis rejected with400. - Returns
400if a parameter value is invalid (for examplepageSize> 200, asubscriptionTypeIdthat is not a valid ID, or anunsubscribedAfterthat is not a valid ISO 8601 timestamp). pageNum×pageSizemust not exceed10,000, so at most 10,000 opt-outs are reachable in a single pass. Narrow the read withsubscriptionTypeId,scope, orunsubscribedAfterif your community has more than that. Requests past the limit are rejected with400.
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:
- Polling with
unsubscribedAfterre-delivers anallrow you already have whenever that member edits an unrelated preference. Harmless if you apply rows as upserts, which we recommend anyway. - Within a single paged read, an
allrow 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=alland a largepageSize, 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 communitycurl -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 storedcurl -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 newslettercurl -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 emailscurl -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,trueorfalse, defaults tofalse.falsereturns only the types currently in use;truealso returns retired types.systemKey, string, only return the system-created type with this key, for exampleclub_announcement. Exact match.pageNum, integer (≥ 1), the page number to retrieve (1-based), defaults to1pageSize, integer (1–200), number of types per page, defaults to50
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 (echoespageNum)pageSize, number, the page size that was appliedresults, 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 assubscriptionTypeIdand accepts as itssubscriptionTypeIdfilter.name, string, the type's name, as members see itdescription, string, the description shown to members on the preference page.nullwhen unset.hidden, boolean,truewhen the type has been retiredcreatedAt, string (ISO 8601), when the type was createdupdatedAt, 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
nameas a join key. Match onidinstead. - Deleting a subscription type in the dashboard retires it — it is marked
hidden: truerather than erased, so opt-outs recorded against it are preserved. Those opt-outs still appear in the feed, so passincludeHidden=truehere when you need to resolve every ID the feed can return. includeHiddenaccepts only the literal stringstrueandfalse;1and0are rejected with400.- An unknown
systemKey, or a community with no subscription types at all, returns an emptyresultsarray, not a404. - Each query parameter may be supplied at most once. A repeated parameter is rejected with
400. - Returns
400if a parameter value is invalid, for examplepageSize> 200. - Most communities have only a handful of types, so one page is normally enough.