Skip to main content

Public REST API reference

The Scale plan unlocks a public REST API at https://vltrx-rewards-api.valotrix.com/api/v1/. This reference walks every endpoint, every parameter, and every response shape - what to send, what you'll get back, what every error code means.

Use cases

  • Build a custom analytics dashboard on top of Valotrix Cart Rewards data.
  • Programmatically import campaign recipes (e.g. seed a fresh dev store with a tested set).
  • Pipe gift events from a headless storefront into Valotrix Cart Rewards's analytics.
  • Pull per-customer redemption counts to enforce limits in a custom checkout flow.
  • Pull aggregated daily stats into your data warehouse.
  • Trigger campaign enable/disable from external schedulers.

API access requires the Scale plan. Free, Growth, and Pro plans see the API & Webhooks section in Settings but can't generate working keys - attempts return 403.

Authentication

Every request needs an Authorization header with a Bearer token:

Authorization: Bearer vltrx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Generate keys in the admin under Settings → API & Webhooks → API Keys. Each key is shown once at creation - copy it immediately. We can't recover a lost key (keys are SHA-256 hashed at rest); revoke and create a new one.

A missing or invalid token returns 401:

{ "error": "Unauthorized" }

A valid token whose plan doesn't include API access returns 403:

{ "error": "API access requires the Scale plan" }

Response headers

Every response includes:

  • X-Request-Id: <uuid> - unique per request. Include this when emailing support.
  • Content-Type: application/json (except 204 No Content).
  • Cache-Control: private, max-age=N on a few read endpoints (/rules, /redemptions).

Rate limits

Rate-limit budgets per API key (sliding 60-second window):

Endpoint groupLimitNotes
Most read/write endpoints120 / min/campaigns, /campaigns/:id, /rules, /widgets, /redemptions, enable/disable/schedule
/analytics30 / minTighter - analytics queries can scan up to 90 days of stats
/events (POST ingest)60 / min
/campaigns/import10 / minStrictest - each request can create up to 50 campaigns

Exceeding a limit returns 429 with a Retry-After header (seconds):

HTTP/1.1 429 Too Many Requests
Retry-After: 47
{ "error": "Rate limit exceeded" }

Pagination

List endpoints (/campaigns) use page-based pagination:

Query paramDefaultMaxNotes
page1-1-indexed
perPage50100Clamped if you exceed

The response includes a meta block:

{
"campaigns": [ /* … */ ],
"meta": { "total": 213, "page": 2, "perPage": 50 }
}

To page through everything, increment page until your accumulated count reaches meta.total (or you get fewer rows than perPage).

Idempotency

Mutation endpoints (POST, PUT, DELETE) don't currently use an Idempotency-Key header - replays may be applied twice. Use the X-Request-Id returned in the response to track and reconcile from your side. Idempotency-Key support is on the roadmap.

Errors

All errors return a JSON object with at minimum an error field:

{ "error": "Campaign not found" }

Some endpoints add structured fields like issue and path for validation errors:

{
"error": "Invalid widget settings body",
"issue": "Required",
"path": ["progressBar", "thresholds", 0, "amountCents"]
}

Status-code-to-meaning map:

StatusMeaning
400Invalid request body, missing query param, malformed payload
401Missing or invalid Authorization header
403Authenticated but plan doesn't grant access (Scale required)
404Resource not found for this shop
405HTTP method not allowed (response includes Allow: GET, PUT, DELETE)
409Plan-cap exceeded on import / create (e.g. trying to add 51st campaign on a plan that allows 50)
413Request body too large
429Rate limit exceeded - see Retry-After
500Internal server error - email support with X-Request-Id

Endpoints

Campaigns: list

GET /api/v1/campaigns

Query params:

NameTypeNotes
pageint1-indexed; default 1
perPageintdefault 50, max 100
enabled"true" | "false"filter by enabled state
curl -H "Authorization: Bearer $VLTRX_KEY" \
"https://vltrx-rewards-api.valotrix.com/api/v1/campaigns?enabled=true&perPage=20"
const res = await fetch(
"https://vltrx-rewards-api.valotrix.com/api/v1/campaigns?enabled=true",
{ headers: { Authorization: `Bearer ${process.env.VLTRX_KEY}` } }
);
const { campaigns, meta } = await res.json();

Response 200:

{
"campaigns": [
{
"id": "clxabc123",
"name": "Spend $50, get a free tote bag",
"enabled": true,
"isDraft": false,
"rulesCount": 1,
"startAt": null,
"endAt": null,
"createdAt": "2026-04-12T10:30:00.000Z",
"updatedAt": "2026-05-08T14:22:11.000Z"
}
],
"meta": { "total": 1, "page": 1, "perPage": 50 }
}

Campaigns: bulk import

POST /api/v1/campaigns/import

Bulk-creates campaigns. Does not update existing campaigns - every entry creates a new campaign. Up to 50 campaigns per request.

Plan-cap behavior: the import is rejected as a whole batch (403) if it would push you above your plan's campaign or rule cap. Plan-gated features (e.g. customer-tag conditions on a Free shop) are silently stripped per enforcePlanFeatures.

Request body:

{
"campaigns": [
{
"name": "Imported recipe",
"enabled": true,
"isDraft": false,
"startAt": null,
"endAt": null,
"rules": [
{
"name": "Tier 1",
"enabled": true,
"conditionTree": {
"type": "cart.subtotal_gte",
"value": 5000,
"currencyOverrides": { "EUR": 4500 }
},
"giftConfig": {
"type": "fixed",
"gifts": [
{ "variantId": "gid://shopify/ProductVariant/12345", "quantity": 1 }
],
"fallbackOnOOS": false
},
"options": {
"actionType": "auto_add",
"discountMethod": "automatic",
"discountType": "percentage",
"discountValue": 100,
"discountTitle": "FREE GIFT"
}
}
]
}
]
}

Response 200:

{
"imported": 1,
"campaignIds": ["clxnewid001"]
}

Errors:

  • 400 - missing or empty campaigns array, or > 50 campaigns
  • 403 - plan-cap exceeded (rejects the whole batch - fix the input or upgrade)
  • 400 with issue field - schema validation failure on a specific entry

Campaign: get one

GET /api/v1/campaigns/:id

Returns the campaign with all its rules inline (rules' conditionTree, giftConfig, and options are returned as parsed JSON objects, not strings).

curl -H "Authorization: Bearer $VLTRX_KEY" \
https://vltrx-rewards-api.valotrix.com/api/v1/campaigns/clxabc123

Response 200:

{
"campaign": {
"id": "clxabc123",
"name": "Spend $50, get a free tote bag",
"enabled": true,
"isDraft": false,
"startAt": null,
"endAt": null,
"createdAt": "2026-04-12T10:30:00.000Z",
"updatedAt": "2026-05-08T14:22:11.000Z",
"rules": [
{
"id": "ruleabc",
"name": "Tier 1",
"enabled": true,
"conditionTree": { "type": "cart.subtotal_gte", "value": 5000 },
"giftConfig": { "type": "fixed", "gifts": [/* … */], "fallbackOnOOS": false },
"options": {
"actionType": "auto_add",
"discountMethod": "automatic",
"discountType": "percentage",
"discountValue": 100,
"discountTitle": "FREE GIFT"
},
"createdAt": "2026-04-12T10:30:00.000Z",
"updatedAt": "2026-04-12T10:30:00.000Z"
}
],
"experiment": null
}
}

If an A/B experiment is active on this campaign, the experiment field is populated with { id, status, primaryMetric, startedAt, stoppedAt, concludedAt, winnerVariantId, variants: [...] }.

Errors:

  • 404 - campaign not found (or belongs to a different shop)

Campaign: full replace

PUT /api/v1/campaigns/:id

Replaces the campaign in full (REST semantics - all fields are required). Body shape mirrors the GET response (single campaign with inline rules).

Plan-gated features get stripped via enforcePlanFeatures if your plan doesn't include them. Plan-cap counts exclude the campaign you're updating, so PUT-ing changes to an existing campaign won't trip the cap.

Webhooks fire only on enabled-state transitions (campaign.activated / campaign.deactivated).

Request body: same shape as GET /api/v1/campaigns/:id returns under campaign.

Response 200: same shape as the GET (returns the updated campaign).

Errors:

  • 400 - body fails campaignSchema validation (response has issue + path)
  • 403 - plan-cap exceeded (e.g. the new rule count would exceed cap)
  • 404 - campaign not found
  • 405 - wrong method (response includes Allow: GET, PUT, DELETE)

Campaign: delete

DELETE /api/v1/campaigns/:id

Deletes the campaign and all its events. If the campaign was live (enabled && !isDraft), a campaign.deactivated webhook fires with trigger: "api" (there's no separate campaign.deleted event - deletion is a stronger form of deactivation).

curl -X DELETE -H "Authorization: Bearer $VLTRX_KEY" \
https://vltrx-rewards-api.valotrix.com/api/v1/campaigns/clxabc123

Response 200:

{ "ok": true }

Errors:

  • 404 - campaign not found

Campaign: enable

POST /api/v1/campaigns/:id/enable

Enables a campaign. Flips isDraft → false, enforces the plan cap on active campaigns, runs a health-check gate on gift inventory + trigger products, invalidates the rules-payload cache, syncs the automatic discount, and dispatches a campaign.activated webhook.

Query params:

NameNotes
force=1Bypass the health-check gate (use sparingly - broken-gift campaigns will fire on the storefront anyway)
curl -X POST -H "Authorization: Bearer $VLTRX_KEY" \
https://vltrx-rewards-api.valotrix.com/api/v1/campaigns/clxabc123/enable

Response 200:

{
"ok": true,
"campaign": {
"id": "clxabc123",
"enabled": true,
"isDraft": false
}
}

If the health check fails and force isn't set:

HTTP/1.1 422 Unprocessable Entity
{
"error": "Campaign has health issues",
"warnings": [
{ "type": "gift_oos", "ruleId": "ruleabc", "variantId": "gid://shopify/ProductVariant/12345" }
]
}

Pass ?force=1 to skip the gate.

Errors:

  • 403 - campaign-cap exceeded (e.g. activating this would put you above the plan's max active campaigns)
  • 404 - campaign not found

Campaign: disable

POST /api/v1/campaigns/:id/disable

Disables a campaign. Invalidates the rules-payload cache, syncs the automatic discount, and dispatches a campaign.deactivated webhook with trigger: "api".

curl -X POST -H "Authorization: Bearer $VLTRX_KEY" \
https://vltrx-rewards-api.valotrix.com/api/v1/campaigns/clxabc123/disable

Response 200:

{
"ok": true,
"campaign": {
"id": "clxabc123",
"enabled": false
}
}

Campaign: schedule

POST /api/v1/campaigns/:id/schedule

Sets startAt and/or endAt. Both are optional; passing null clears the existing value. If both are provided, endAt must be strictly after startAt.

Request body:

{
"startAt": "2026-11-29T00:00:00.000Z",
"endAt": "2026-12-02T23:59:59.000Z"
}

Or to clear the schedule:

{ "startAt": null, "endAt": null }

Timestamps must be ISO 8601 strings. (For YYYY-MM-DD-only ranges, append T00:00:00.000Z.)

Response 200:

{
"ok": true,
"campaign": {
"id": "clxabc123",
"startAt": "2026-11-29T00:00:00.000Z",
"endAt": "2026-12-02T23:59:59.000Z"
}
}

Errors:

  • 400 - body missing both startAt and endAt, invalid ISO 8601, or endAt <= startAt

Rules (active payload)

GET /api/v1/rules

Returns the active rules payload - the same shape served to the storefront engine, but accessed via API key instead of the storefront proxy. Useful for replicating rule logic in a headless storefront or for debugging.

Supports HTTP caching via ETag and If-None-Match:

# First request
curl -i -H "Authorization: Bearer $VLTRX_KEY" \
https://vltrx-rewards-api.valotrix.com/api/v1/rules
# Response includes: ETag: "abc123", Cache-Control: private, max-age=60

# Subsequent request - use the etag
curl -i -H "Authorization: Bearer $VLTRX_KEY" \
-H 'If-None-Match: "abc123"' \
https://vltrx-rewards-api.valotrix.com/api/v1/rules
# Response: 304 Not Modified, no body

Response body shape (200):

{
"rules": [ /* active rules with parsed conditionTree, giftConfig, options */ ],
"experiments": [ /* active experiment configs, projected to engine shape */ ],
"debugAllowed": true,
"shopSettings": {
"engine": { "runOnExpressCheckout": true, "runOnCartSubmission": false, "useDiscountedPrices": false, "evaluationDelay": 300 },
"checkout": { "blockZeroValueCart": true, "blockGiftOnlyCarts": true, "stripZeroPriceItems": true },
"data": { "collectCartTokens": true }
},
"widgets": { /* widget settings (toast, popup, progressBar, etc.) */ }
}

experiments is omitted (not just empty) when the shop has no active experiments.


Widgets: get

GET /api/v1/widgets

Returns the shop's widget configuration.

curl -H "Authorization: Bearer $VLTRX_KEY" \
https://vltrx-rewards-api.valotrix.com/api/v1/widgets

Response 200:

{
"widgets": {
"_version": 14,
"toast": { "enabled": true, /* ... */ },
"promoPopup": { /* ... */ },
"progressBar": { /* ... */ },
"giftChoice": { /* ... */ },
"reminder": { /* ... */ }
}
}

Widgets: full replace

PUT /api/v1/widgets

Full replace of widget settings. Partial updates are not supported - GET, modify, PUT. The server stamps the canonical _version on every save (any client-supplied _version is ignored to prevent migration-skip drift).

Request body: same shape as the GET response under widgets.

Response 200:

{ "ok": true, "widgets": { /* the persisted version */ } }

Custom-code fields (HTML/CSS/JS in customCode) are persisted as-is. The storefront proxy strips them on the way out for non-Scale shops, so plan gating is enforced at delivery, not write.

Errors:

  • 400 - body fails widgetSettingsSchema validation (response has issue + path)
  • 405 - wrong method (response includes Allow: GET, PUT)

Redemptions

GET /api/v1/redemptions?customerId=<numeric-id>

Returns per-customer redemption counts. Used to enforce per-customer limits in custom checkout flows. Cached per request for 30 seconds (Cache-Control: private, max-age=30).

Required query params:

NameTypeNotes
customerIdnumeric stringShopify customer ID - must be all digits
curl -H "Authorization: Bearer $VLTRX_KEY" \
"https://vltrx-rewards-api.valotrix.com/api/v1/redemptions?customerId=987654321"

Response 200:

{
"redemptions": [
{ "ruleId": "ruleabc", "campaignId": "clxabc123", "count": 1, "lastRedeemedAt": "2026-05-08T14:22:11.000Z" }
]
}

Errors:

  • 400 - missing customerId or non-numeric value

Events ingest (headless storefronts)

POST /api/v1/events

For headless storefronts that don't run the standard Valotrix Cart Rewards storefront script. Ingests gift-added / gift-removed / choice-shown / choice-selected events directly from your custom storefront, mirroring what the standard storefront proxy ingests.

Standard Shopify storefronts don't need this - they go through the storefront proxy automatically. Use this only when you've built your own storefront on top of Shopify's Storefront API.

Request body: validated against proxyEventSchema - the same payload the proxy event endpoint accepts. Has events: [...] plus storefront context.

Response 200:

{ "ok": true, "ingested": 3 }

Errors:

  • 400 - body fails schema validation
  • 429 - rate-limited (60/min)

Analytics

GET /api/v1/analytics?from=YYYY-MM-DD&to=YYYY-MM-DD

Returns daily totals + per-campaign stats over a date range.

Required query params:

NameFormatNotes
fromYYYY-MM-DDinclusive
toYYYY-MM-DDinclusive

Optional:

NameNotes
campaignIdfilter campaignStats to one campaign

Maximum range: 90 days. Larger ranges return 400.

curl -H "Authorization: Bearer $VLTRX_KEY" \
"https://vltrx-rewards-api.valotrix.com/api/v1/analytics?from=2026-04-01&to=2026-04-30"

Response 200:

{
"dailyTotals": [
{ "date": "2026-04-01", "totalOrders": 142, "totalRevenue": 8420.50, "ordersWithGifts": 38, "revenueWithGifts": 2890.00 }
],
"campaignStats": [
{ "date": "2026-04-01", "campaignId": "clxabc123", "ordersWithGifts": 38, "giftsGiven": 38, "revenueWithGifts": 2890.00 }
],
"summary": {
"totalOrders": 1180,
"totalGifts": 1180,
"totalRevenue": 86420.30
}
}

Notes:

  • Currency is your shop's base currency.
  • Revenue values are dollars-with-decimals (not cents).
  • An order's contribution lands on the day the order completed, in the shop's timezone.

Errors:

  • 400 - missing from / to, invalid date format, range > 90 days, or from > to

What's not in v1

Documented for transparency:

  • GraphQL - REST only in v1.
  • Scoped keys - every key currently has full access. Per-resource scopes are on the roadmap.
  • Idempotency-Key - replays may apply twice on POST/PUT/DELETE.
  • Cursor pagination - page-based only. Stable enough for most workflows.
  • Webhook subscription management via API - webhook endpoints are configured in Settings only.

Next: Outbound webhooks →