Appearance
Get Subscribers
Fans who opt in through a smart link — email capture or a presave — are stored as opt-ins linked to a fan record. Both are readable through the API, which is how you sync an artist's audience into an ESP, CRM, or warehouse.
Data model
| Table | Holds |
|---|---|
optins | One row per opt-in event: which artist, which link, which channel, what value, current delivery status. |
fans | The person: email, location, Spotify connection. One fan can have many opt-ins. |
presave_subscriptions | Presave signups tied to a specific release link. |
optouts | Unsubscribe records, per artist and platform. |
Opt-ins are read-only through the API. They are created by visitors on the link pages, not by API clients.
List an artist's subscribers
graphql
query Subscribers($artist_id: uuid!, $limit: Int = 100, $offset: Int = 0) {
optins(
where: { artist_id: { _eq: $artist_id } }
order_by: { created_at: desc }
limit: $limit
offset: $offset
) {
id
type
value
status
platform
country
region
city
future_releases
imported
created_at
link { id domain path title }
fan {
id
email
country
spotify_connected
}
}
optins_aggregate(where: { artist_id: { _eq: $artist_id } }) {
aggregate { count }
}
}optins fields
| Field | Type | Description |
|---|---|---|
id | uuid! | Opt-in ID. |
artist_id | uuid | Artist the fan opted in to. |
type | String | Capture channel. |
value | String | The captured email address. |
status | String | Delivery/subscription state. |
platform | String | Where the opt-in originated. |
country, region, city | String | Geolocation at capture time. |
future_releases | Boolean | Fan agreed to be told about future releases. |
imported | Boolean | Row came from a bulk import rather than a live capture. |
fail_count | Int | Failed delivery attempts. |
last_try | timestamptz | Last delivery attempt. |
created_at | timestamptz | When the fan opted in. |
link_id | uuid | Link where the opt-in happened. |
source_link_id | uuid | Originating link, when it differs. |
link_destination_id | uuid | Destination button that triggered it. |
fan_id | uuid | The fans row. |
fans fields
| Field | Type | Description |
|---|---|---|
id | uuid! | Fan ID. |
email | String | Email address. |
country, region, city | String | Location. |
spotify_connected | Boolean | Fan connected a Spotify account. |
spotify_email | String | Email from the connected Spotify account. |
You can query fans directly; visibility is scoped to fans who have opted in to or presaved for one of your artists.
graphql
query FansWithSpotify {
fans(where: { spotify_connected: { _eq: true } }, limit: 100) {
id
email
country
spotify_connected
}
}Common filters
graphql
# Email subscribers only
where: { artist_id: { _eq: $artist_id }, type: { _eq: "email" } }
# Opted in to future releases
where: { artist_id: { _eq: $artist_id }, future_releases: { _eq: true } }
# Captured since a timestamp — the basis of an incremental sync
where: { artist_id: { _eq: $artist_id }, created_at: { _gt: $since } }
# From one link
where: { link_id: { _eq: $link_id } }
# One market
where: { artist_id: { _eq: $artist_id }, country: { _eq: "US" } }
# Exclude bulk-imported rows
where: { artist_id: { _eq: $artist_id }, imported: { _eq: false } }Incremental export
Walk forward by created_at rather than paging with offset — it stays correct while new opt-ins arrive mid-export.
graphql
query ExportSince($artist_id: uuid!, $since: timestamptz!) {
optins(
where: {
artist_id: { _eq: $artist_id }
created_at: { _gt: $since }
}
order_by: { created_at: asc }
limit: 500
) {
id
type
value
country
created_at
fan { email }
}
}Store the highest created_at you received and pass it as $since next run. Keep the page size modest; these are large tables.
Counting
graphql
query Counts($artist_id: uuid!) {
total: optins_aggregate(where: { artist_id: { _eq: $artist_id } }) {
aggregate { count }
}
email: optins_aggregate(
where: { artist_id: { _eq: $artist_id }, type: { _eq: "email" } }
) {
aggregate { count }
}
}Presaves
Presave signups are tracked separately, per release link:
graphql
query Presaves($artist_id: uuid!) {
presave_subscriptions(
where: { artist_id: { _eq: $artist_id } }
order_by: { created_at: desc }
limit: 100
) {
id
country
region
city
created_at
link { id path title }
}
}Richer per-fan presave detail, including status and delivery attempts, is in presaves:
graphql
query PresaveDetail($link_id: uuid!) {
presaves(where: { link_id: { _eq: $link_id } }) {
id
type
status
emails
future_releases
fail_count
last_try
country
created_at
fan { id email }
}
}Opt-outs
Always reconcile against opt-outs before sending anything.
graphql
query OptOuts($artist_id: uuid!) {
optouts(where: { artist_id: { _eq: $artist_id } }) {
id
platform
created_at
}
}WARNING
Opt-in records are personal data. Exporting them makes you responsible for handling under GDPR, CCPA, and the consent terms the fan actually agreed to — which is per-artist, not per-account. Honour optouts, respect future_releases, and do not merge one artist's list into another's.
Not available through the API
Opt-ins cannot be created, edited, or deleted through the API — the permission model grants manager read access only. Capture happens on the link pages; deletion requests go through support so that suppression records are kept correctly.