Skip to content
Browse the docs

API

Redemptions

One endpoint, two ways to read it: everything in order, or a range with filters.

Endpoints

GET/redemptionsAs a feed (after, limit) or by range (preset or from/through, plus filters).
GET/redemptions/{id}One redemption.

Keeping a complete copy

With no parameters, the feed starts at the beginning and walks every redemption in the order Viffy recorded it. Save the cursor between runs.

Sync loop
cursor = load_saved_cursor()            // none on the first run

loop:
  page = GET /redemptions?limit=1000&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)
  • Exclusions and restorations update the same redemption, but its recordedAt and feed position do not change. Forward polling alone misses corrections to rows before your cursor.
  • Subscribe to redemption.excluded and redemption.restored, or read those changes from the event log. Fetch GET /redemptions/{id} after a correction so out-of-order events cannot overwrite newer state.
  • Order is by recordedAt, when Viffy learned of the redemption.

Reconcile corrections

Keep the last non-null cursor; if the feed has never returned one, start again without after. Event-log checkpoints are separate from redemption-feed checkpoints.

Reconcile your copy with creator counts before crediting creators. The event log retains 30 days; after a longer interruption, re-read affected redemptions or the full feed to recover current exclusions and restorations.

Excluding and restoring redemptions are actions for your team in Viffy. The partner API reads those changes but has no write endpoint for them.

Reading a range

Range mode filters by redeemedAt, the sale time, and lists the newest sales first using your reporting time zone. Send any parameter below to use range mode.

ParameterWhat it is
preset / from / throughChoose a preset or a first and last day.
brandId / campaignId / countingStateFilter by brand, campaign, or counting status.
page / pageSize / asOfChoose a page and keep the same asOf value on later pages.

Do not combine these with the feed parameters after or limit.

Request
GET /redemptions?preset=7d&countingState=counted&page=0&pageSize=200
200 response
{
  "range": {
    "from": "2026-09-05",
    "through": "2026-09-11",
    "timeZone": "America/Chicago",
    "preset": "7d"
  },
  "asOf": "2026-09-11T15:04:05Z",
  "items": [
    {
      "id": "e3a954b1-820c-49d7-a056-cc7f2d9e613a",
      "serializedGs1": "8112000000000000000000000000",
      "redeemedAt": "2026-09-11T14:59:12Z",
      "recordedAt": "2026-09-11T15:04:05Z",
      "campaign": {
        "id": "7c1e29a4-5b68-4d02-91f3-8a4c6e0b752d",
        "externalReference": "fall-launch-2026",
        "name": "Fall launch"
      },
      "offer": {
        "baseGs1": "8112000000000000000",
        "title": "$1 off any two"
      },
      "brand": {
        "id": "9a2b835d-674e-48ab-b012-a93b6407c2e8",
        "name": "Acme Foods"
      },
      "creator": {
        "externalId": "hb-1042",
        "name": "Jane Doe"
      },
      "retailer": null,
      "countingState": "counted",
      "exclusion": null
    }
  ],
  "page": 0,
  "pageSize": 200,
  "totalCount": 1
}

Reuse the returned asOf on later pages to leave out redemptions whose recordedAt is later than that time. Later corrections and name changes can still appear; restart if a correction affects your filters.

Range mode accepts up to 366 calendar days, including the first and last day.

A redemption

Item
{
  "id": "e3a954b1-820c-49d7-a056-cc7f2d9e613a",
  "serializedGs1": "8112000000000000000000000000",
  "redeemedAt": "2026-09-11T14:59:12Z",
  "recordedAt": "2026-09-11T15:04:05Z",
  "campaign": {
    "id": "7c1e29a4-5b68-4d02-91f3-8a4c6e0b752d",
    "externalReference": "fall-launch-2026",
    "name": "Fall launch"
  },
  "offer": {
    "baseGs1": "8112000000000000000",
    "title": "$1 off any two"
  },
  "brand": {
    "id": "9a2b835d-674e-48ab-b012-a93b6407c2e8",
    "name": "Acme Foods"
  },
  "creator": {
    "externalId": "hb-1042",
    "name": "Jane Doe"
  },
  "retailer": null,
  "countingState": "counted",
  "exclusion": null
}
FieldTypeMeaning
idstring (uuid)Redemption ID; distinct from the envelope event ID.
serializedGs1stringThe redeemed code. Keep it as text to preserve every digit.
redeemedAtstring (date-time)When the redemption happened.
recordedAtstring (date-time)When Viffy first recorded the redemption; the redemption feed sorts by this time.
campaignCampaignReferenceCampaign reference; fields are defined below.
offerOfferReferenceOffer identifier and title.
brandBrandReferenceBrand identifier and name.
creatorCreatorReference | nullAttributed creator; null only when the code reached no shopper.
retailerRetailerReference | nullCampaign retailer restriction, or null when unrestricted.

Nested campaign, offer, brand, creator, and retailer fields are defined in Shared objects.

FieldWhat it is
countingStatecounted, excluded, or test_code. Only counted rows contribute to verified totals. Viffy does not issue payments; use creator-counts to reconcile creator credit.
exclusionObject { excludedAt: UTC timestamp, reason: string or null }, or null when not excluded. excluded takes precedence over test_code as a countingState; an excluded test-code redemption has this exclusion object.
creatorThe creator whose link handed out the code, fixed at that moment. Null only for a code that reached no shopper.
retailerThe campaign’s retailer restriction, when set. It does not identify the store where the purchase happened.
serializedGs1The redeemed code. The partner API does not return unredeemed codes.