API
Surveys and sign-ups
Read survey answers and email sign-ups into your own tools.
Endpoints
GET
/campaigns/{id}/surveyA campaign’s survey, with every version it has published and each question’s and answer’s key.GET
/survey-responsesEvery response sent in the last 90 days, as a feed (after, limit, campaignId).GET
/survey-responses/{id}One response.GET
/email-signupsThe email sign-ups of one day that has ended (day, after, limit).All four are read only. Surveys are built and reviewed in Viffy.
Keeping a copy of answers
The feed lists responses oldest first. A response joins it a minute after it is sent, and leaves it 90 days after.
Sync loop
cursor = load_saved_cursor() // none on the first run
loop:
page = GET /survey-responses?limit=500&after=cursor // omit after on the first run
store_and_commit(page.items) // upsert by id before checkpointing
if page.nextCursor is null: break // the last page; caught up
cursor = page.nextCursor
save_cursor(cursor)- Map answers by question key and answer key, not by wording. Keys stay the same across versions and across campaigns a question was copied to.
- The survey_response.recorded event tells you as soon as a shopper sends answers. Fetch
GET /survey-responses/{id}to read them. - A response carries no email address.
- Written answers are deleted 90 days after a shopper sends them, and email addresses 90 days after they’re given. Other answers are kept without anything that identifies the shopper.
Email sign-ups, a day at a time
Sign-ups come from their own endpoint, one UTC day at a time. A day is listed once it has ended.
| Parameter | What it is |
|---|---|
day | The UTC day to read, as yyyy-MM-dd. Leave it out for yesterday. Today and later days are refused. |
after / limit | Read with after until nextCursor is null. A cursor works only for the day it came from. |
Daily import
day = last_day_imported + 1 // yesterday on the first run
while day < today_in_utc:
cursor = none
loop:
page = GET /email-signups?day=day&limit=500&after=cursor
import_into_email_tool(page.items)
if page.nextCursor is null: break
cursor = page.nextCursor
save_last_day_imported(day)
day = day + 1- A sign-up carries the email address, the brand, the day, and the consent the shopper ticked. It carries no campaign, creator or time, and a day’s sign-ups come in no particular order.
- That keeps a sign-up from being matched to anyone’s survey answers. The shopper agreed to hear from the brand, not to have their answers tied to their email.
- No event announces a sign-up. Read each finished day once.
- A sign-up’s email address is deleted 90 days after it was given. Import each day before then.
- A shopper signs up on a card of their own, apart from the survey, so a sign-up may come from a shopper who sent no answers.
- The brand sends every email, and handles unsubscribes in its own email tool.
When a shopper asks to delete their answers
- Viffy deletes the shopper’s answers and sends survey_response.removed with the response’s id. Delete your copy of that response.
- The event doesn’t name the email address. If the shopper signed up, Viffy forwards their request to you, so you can remove them from your email list.
- After 90 days, nothing in Viffy identifies the shopper, so no later event arrives.
A response and a sign-up
Survey response
{
"id": "5d0c7a41-8b2e-4f6a-9c13-2e7b8f4a6d90",
"campaign": {
"id": "7c1e29a4-5b68-4d02-91f3-8a4c6e0b752d",
"externalReference": "fall-launch-2026",
"name": "Fall launch"
},
"creator": {
"externalId": "cr-1042",
"name": "Jane Doe"
},
"surveyVersionId": "1a7e5c90-3b4d-4f21-8e6a-9c0d2b7f4e13",
"surveyVersion": 2,
"submittedAt": "2026-09-11T15:04:05Z",
"answers": [
{
"questionKey": "age_range",
"optionKeys": [
"25_34"
],
"number": null,
"text": null
},
{
"questionKey": "zip_code",
"optionKeys": [],
"number": null,
"text": "60614"
}
]
}| Field | What it is |
|---|---|
answers | One entry per question answered. A choice names its answer keys; a whole number or rating carries number; a written answer or ZIP code carries text. |
surveyVersion | The version the shopper answered. Earlier answers keep the wording they were given. |
Email sign-up
{
"email": "[email protected]",
"brand": {
"id": "3c9e1f20-7a4b-4d6e-8f13-2b5c7d9e0a41",
"name": "Celsius"
},
"signedUpOn": "2026-09-11",
"consentWording": "Email me offers and news from Celsius. I'm 18 or older. I can unsubscribe at any time.",
"brandPrivacyPolicy": "https://celsius.example/privacy"
}| Field | What it is |
|---|---|
signedUpOn | The UTC day the shopper signed up. There is no time of day. |
consentWording | The sentence the shopper ticked, with the brand’s name. |
brandPrivacyPolicy | The brand privacy policy linked beside it. |