Skip to content
Browse the docs

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.

ParameterWhat it is
dayThe UTC day to read, as yyyy-MM-dd. Leave it out for yesterday. Today and later days are refused.
after / limitRead 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"
    }
  ]
}
FieldWhat it is
answersOne 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.
surveyVersionThe 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"
}
FieldWhat it is
signedUpOnThe UTC day the shopper signed up. There is no time of day.
consentWordingThe sentence the shopper ticked, with the brand’s name.
brandPrivacyPolicyThe brand privacy policy linked beside it.