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 not allowed - plan doesn't grant access (Scale required), or a plan cap was exceeded on import / create / enable
404Resource not found for this shop
405HTTP method not allowed (response includes Allow: GET, PUT, DELETE)
409Health-check gate blocked a campaign enable (error: "health_warnings") - re-send with ?force=1 to override
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). Rules that include their id (as returned by GET) are updated in place; rules omitted from the body are deleted; rules without an id are created. Keep the id fields from the GET response when round-tripping - it preserves rule identity for A/B experiments and analytics history.

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)
  • 409 - the update would remove or replace a rule that is part of a running A/B experiment; stop the experiment first

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 409 Conflict
{
"error": "health_warnings",
"warnings": [
{
"variantId": "12345",
"variantGid": "gid://shopify/ProductVariant/12345",
"displayName": "Canvas Tote - Natural",
"productTitle": "Canvas Tote",
"inventoryQuantity": 0,
"inventoryPolicy": "DENY",
"level": "out_of_stock"
}
],
"message": "Publish blocked by health warnings. Re-send with ?force=1 to override."
}

The warnings array mixes gift inventory warnings (shape above, level is out_of_stock or low_stock), gift product-status warnings (status: draft / archived / deleted), and trigger-product warnings (same status field). 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, "count": 3 }

count is the number of events accepted from the batch.

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 →