API
Offers and campaigns
Read a brand’s offers, then create a campaign with your own dates, code limit, and commission settings.
Endpoints
/offersEvery offer you may distribute. Filters: brandId, state./offers/{baseGs1}One offer./campaignsYour campaigns, newest first. Filters: status, brandId, externalReference./campaigns/{id}One campaign./campaignsCreate a draft. Returns 201./campaigns/{id}Change only what you send./campaigns/{id}/statusActivate, pause or end./campaigns/{id}/reportIts funnel over a range. See Reports.Offers
An offer’s state is one of available, not_yet_started, expired, temporarily_unavailable, not_distributable. Only available offers (canDistribute: true) accept a new campaign.
{
"baseGs1": "8112000000000000000",
"brand": { "id": "9a2b835d-674e-48ab-b012-a93b6407c2e8", "name": "Acme Foods" },
"title": "$1 off any two", "description": "Save on two qualifying products.",
"state": "available", "canDistribute": true,
"savings": { "cents": 100, "display": "$1.00" },
"productBarcodes": ["012345678905"],
"campaignDates": { "from": "2026-09-01", "through": "2026-09-30" },
"redemptionDates": { "from": "2026-09-01", "through": "2026-10-31" },
"codesAvailable": 10000,
"retailerChoiceRequired": false, "allowedRetailers": ["kroger.com"],
"lastUpdatedAt": "2026-09-11T15:04:05Z"
}| Field | What it is |
|---|---|
baseGs1 / brand | Offer identifier string and brand { id, name }. Keep baseGs1 as text; brand.id is the brandId filter value. |
title / description | Strings, or null if the brand did not supply them. |
state / canDistribute | State string above and boolean saying whether a new campaign can be created. |
savings | Object { cents: integer, display: string }, or null if no shopper savings amount is available. cents is not dollars. |
productBarcodes | Array of product barcode strings; empty when none were supplied. Preserve leading zeros. |
lastUpdatedAt | UTC timestamp when Viffy last refreshed the offer, even if its details stayed the same. Reading an offer does not trigger a refresh. |
codesAvailable | Integer or null if unknown. Codes left on the offer, shared across campaigns and other distributors. Checked live when you create or raise the limit; creating a campaign does not reserve that supply. |
allowedRetailers | The retailers a campaign may restrict to, as email domains. Empty means no campaign-specific retailer restriction may be selected; shoppers still need a participating retailer that accepts the offer. |
retailerChoiceRequired | True where every campaign on the offer must name a retailer. |
campaignDates / redemptionDates | Each is { from, through } in calendar days, or null when unavailable. Campaign dates you choose must fit inside redemptionDates. |
Create a campaign
POST /campaigns
{
"externalReference": "fall-launch-2026",
"name": "Fall launch",
"baseGs1": "8112000000000000000",
"dates": { "from": "2026-09-01", "through": "2026-09-30" },
"codeLimit": 5000,
"retailer": { "emailDomain": "kroger.com", "name": "Kroger" },
"commission": { "rate": 0.1, "estimatedUnitSalePrice": 4.99 }
}{
"id": "7c1e29a4-5b68-4d02-91f3-8a4c6e0b752d",
"externalReference": "fall-launch-2026",
"name": "Fall launch",
"status": "draft",
"offer": { "baseGs1": "8112000000000000000", "title": "$1 off any two", "brand": { "id": "9a2b835d-674e-48ab-b012-a93b6407c2e8", "name": "Acme Foods" } },
"dates": { "from": "2026-09-01", "through": "2026-09-30" },
"retailer": { "emailDomain": "kroger.com", "name": "Kroger" },
"commission": { "rate": 0.1, "estimatedUnitSalePrice": 4.99, "estimatedPerRedemption": 0.499 },
"codes": { "limit": 5000, "issued": 0, "redeemed": 0, "testCodes": 0, "remaining": 5000 },
"allowedStatuses": ["active", "ended"],
"canChangeRetailer": true,
"createdAt": "2026-08-28T15:04:05Z",
"updatedAt": "2026-08-28T15:04:05Z"
}| Field | Rule |
|---|---|
name | Required. One line, up to 120 characters. |
baseGs1 | Required. An offer from GET /offers. |
dates | Required. Calendar days within the offer’s redemptionDates; through is on or after from. |
codeLimit | Required integer, 1–10,000,000, also bounded by the offer’s available codes. |
retailer | Optional unless retailerChoiceRequired is true. Both emailDomain (one of allowedRetailers) and name, together. |
commission | Required. rate between 0 and 1 (up to 4 decimals); estimatedUnitSalePrice 0 to 100,000 (2 decimals). |
externalReference | Optional, up to 120 characters and unique in your organization. Set it on creation; PATCH cannot change it. |
Send an externalReference so you can find the campaign after a timeout. A repeated create with that reference returns 409; use GET /campaigns?externalReference= to read the existing campaign.
Change a campaign
Send only the fields to change. Absent means unchanged.
PATCH /campaigns/{id}
{ "codeLimit": 8000, "retailer": null }codeLimitis the new total. You can raise it before a campaign ends while the offer is available and has codes left.- You can lower the limit to the number already handed out, including test codes.
retailer: nullclears the restriction; an object with bothemailDomainandnamereplaces it. The retailer identifier becomes fixed when the first code is prepared, shown bycanChangeRetailer: false.- To correct the retailer’s display name after that, send the same
emailDomainwith the new name. datesandcommissionmerge per field. Omit a nested field or send null to keep its current value.- Sending null for
name,dates,codeLimit, orcommissionleaves that field unchanged. - Set
baseGs1andexternalReferencewhen creating the campaign. PATCH does not change them.
Campaign response fields
GET, PATCH, and status changes return the campaign object shown above. Lists wrap these objects in a page, using page and pageSize.
| Field | What it is |
|---|---|
id / externalReference / name / status | Viffy ID, nullable partner reference, display name, and draft, active, paused, or ended status. |
offer | Object { baseGs1, title, brand: { id, name } }; title can be null. |
dates | Object { from, through }, both inclusive calendar days. |
retailer | Object { emailDomain, name }, or null when unrestricted. name can be null on a read. |
commission | Decimals rate, estimatedUnitSalePrice (dollars), and estimatedPerRedemption (dollars). These are planning inputs and an estimate, not payments. |
codes | Integer limit, issued (shoppers), redeemed (verified), testCodes, and remaining. limit = issued + testCodes + remaining; counts are all-time, not a report date range. |
allowedStatuses / canChangeRetailer | Array of next status strings and boolean saying whether the retailer identifier can still change. |
createdAt / updatedAt | UTC timestamps when the campaign was created and last changed. |
Remaining codes describe the limit, not immediate availability. For the full response returned by a campaign’s report, see Reports.
Change status
POST /campaigns/{id}/status
{ "status": "active" }Activate a draft, then pause or resume it as needed. You can end a draft, active, or paused campaign; an ended campaign cannot restart.
allowedStatuses lists the campaign’s next available statuses. Sending its current status returns 200 without changing it.
Paused or ended campaigns
No new codes are handed out. Codes already handed out still work and still count for their creator.