Private beta — the Pixoo API ships with the Enterprise plan. Request access

developers

Pixoo APIv1 private beta

Generate, track and recycle discount codes from your own stack.

The engine behind 200+ Shopify brands, now behind a clean REST API. Mint unique codes one at a time or 100,000 at once, hear back the second one is redeemed, and never babysit a CSV again.

  • REST + JSON
  • Scoped API keys
  • Idempotent writes
  • Signed webhooks
one POST, one fresh unique code
curl -X POST https://api.pixoo.app/api/v1/discounts/1234567890/codes \
  -H "Authorization: Bearer pixoo_live_…" \
  -H "Content-Type: application/json" \
  -d '{"prefix": "WELCOME-"}'

{
  "codes": ["WELCOME-7F3K9Q2M8T1Z"],
  "discount": { "id": "1234567890", "title": "Welcome codes" }
}

Real request, real response shape — copy it once your key exists.

What you can build

Four flows merchants keep asking for

Before any endpoint reference: this is what plugging Pixoo into your stack actually unlocks.

Send a unique welcome code to every new customer

A customer signs up, your backend makes one call, and a fresh unique code lands in their welcome email. No pre-generated pools, no spreadsheet handoffs — one request per customer, the code comes back in the response.

See it in the quick start

Launch a 10,000-code campaign in one call

Influencer drop, print inserts, a partner newsletter — one POST queues the whole batch, and Pixoo pings you the moment every code is ready. Up to 100,000 codes per discount, unique and collision-free.

Read the campaign guide

Know the second a code is redeemed

The code.redeemed webhook delivers the code, the order and the savings — signed, retried, and free of customer PII. Credit the referrer, close the loyalty loop, update your CRM in real time.

Wire up webhooks

Never hit the 100k code cap

Every discount tops out at 100,000 codes — unless you recycle. Collect redeemed codes from webhooks, purge them on a schedule, and the capacity frees up instantly. High-volume stores run this loop forever.

Set up the recycling loop

How it works

The life of a code, end to end

Six moments, two of them where Pixoo calls you. The whole API exists to run this loop.

  1. 1

    Create the discount

    One POST creates a bulk discount — same server-side validation as the Pixoo CSV import.

    POST/discounts

  2. 2

    Generate codes at scale

    Queue up to 100,000 unique codes in a single call. It returns immediately with a batch to track.

    POST/discounts/{id}/code-batches

  3. Pixoo tells you when they’re ready

    webhook — Pixoo calls you

    No polling loops — the code_batch.completed webhook fires the moment the batch lands.

    code_batch.completed

  4. Distribute from your stack

    your stack

    Emails, packing slips, partner portals, loyalty rewards — the codes are yours to route anywhere.

  5. Know the second a code is used

    webhook — Pixoo calls you

    code.redeemed delivers the code, order and savings — signed, retried, no customer PII.

    code.redeemed

  6. 3

    Purge & recycle

    Delete redeemed codes in batches of 250. Capacity is freed instantly.

    POST/discounts/{id}/code-deletions

  7. …and back to step 2

    Purged capacity is freed the moment the deletion is accepted, so you can keep generating. That closed loop is how a busy store never hits the 100,000-code cap.

Quick start

Your first code in three steps

From zero to a fresh, unique discount code — about five minutes, no SDK required.

  1. Create your API key

    In the Pixoo admin, open Integrations → Pixoo API and create a key. Give it a label (“Production backend”) and pick its scopes — discounts:read to look things up, discounts:write to create and delete.

    The pixoo_live_… secret is shown once. Store it in your secret manager — it never appears in the admin again.

  2. Say hello

    GET /whoami is the “is my key wired correctly?” call — it echoes your shop, scopes and rate limits.

    curl https://api.pixoo.app/api/v1/whoami \
      -H "Authorization: Bearer pixoo_live_YOUR_KEY"
    200 — response.json
    {
      "shop": "acme.myshopify.com",
      "key": {
        "label": "Production backend",
        "lastFour": "8T1Z",
        "scopes": ["discounts:read", "discounts:write"],
        "createdAt": "2026-07-10T09:00:00Z",
        "lastUsedAt": "2026-07-10T10:00:00Z"
      },
      "rateLimit": {
        "sustained": { "limit": 120, "windowSeconds": 60 },
        "burst": { "limit": 20, "windowSeconds": 1 }
      }
    }
  3. Mint your first code

    Point the call at any Pixoo bulk discount (create one in the admin, or via POST /discounts) and a fresh unique code comes straight back in the response.

    curl -X POST https://api.pixoo.app/api/v1/discounts/1234567890/codes \
      -H "Authorization: Bearer pixoo_live_YOUR_KEY" \
      -H "Content-Type: application/json" \
      -d '{"prefix": "WELCOME-"}'
    200 — response.json
    {
      "codes": ["WELCOME-7F3K9Q2M8T1Z"],
      "discount": { "id": "1234567890", "title": "Welcome codes" }
    }

All examples use the production base URL https://api.pixoo.app/api/v1— the /v1 contract only changes with a new major version.

Essentials

Four things every integration should know

Auth, errors, quotas and idempotency — the same conventions on all 14 endpoints, so you learn them once.

Authentication

Every request carries your key as a bearer token: Authorization: Bearer pixoo_live_…. Keys are created in the Pixoo admin (Integrations → Pixoo API) and carry scopes: discounts:read for lookups, discounts:write for anything that creates or deletes.

  • Missing or invalid key → 401 unauthorized
  • Valid key, missing scope → 403 forbidden_scope
  • Shop not on an API plan → 403 forbidden_plan

Rate limits

Per key: 120 requests/minute sustained with a burst of 20/second. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and the IETF RateLimit headers.

On 429 rate_limited, honor the Retry-After header (seconds) before retrying. 5xx responses are transient — retry with backoff.

429 — rate limited
HTTP/1.1 429 Too Many Requests
Retry-After: 1
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 0
RateLimit-Policy: 120;w=60, 20;w=1

{"message": "Rate limit exceeded. Retry after 1s.", "code": "rate_limited"}

Idempotency

Every POST that creates discounts or codes accepts an Idempotency-Key header (1–255 chars, unique per shop). Replaying a key returns the stored original response — never a second side effect. Perfect for retry-on-timeout logic.

  • Concurrent duplicate (first request in flight) → 409 conflict
  • A failed request releases the key, so the same key can retry
  • Keys are retained at least 24 hours
  • Keys are endpoint-agnostic — use one key per logical operation, never reuse a key across different calls

Errors

Every non-2xx body has the same two fields: message (human-readable, safe to surface) and code (stable, machine-matchable). Wrong HTTP method → 405 invalid_request.

422 — response.json
{
  "message": "Discount 1234567890 is expired and cannot receive new codes.",
  "code": "invalid_config"
}

All error codes

CodeHTTPMeaning
unauthorized401Missing, unknown or revoked key
forbidden_scope403Key lacks the required scope
forbidden_plan403Shop's plan does not include the API
not_found404Discount / batch / resource not found (or not a Pixoo discount)
invalid_request422Malformed body or params (details in message)
invalid_config422Valid request, unusable target (e.g. discount expired)
cap_reached409Discount is at the 100,000-code cap
conflict409Same Idempotency-Key in flight, or a batch/deletion already running on this discount
rate_limited429Per-key quota exceeded — honor Retry-After
internal_error500Unexpected server error — transient, retry with backoff

Guides

Three recipes you can ship today

Complete, working walkthroughs for the flows from the use cases above — copy, paste, adapt.

Guide 01

Launch a 10,000-code campaign in one call

Anything above 10 codes goes through code batches: one POST queues the generation, Pixoo processes it FIFO per shop, and you collect the codes when it completes.

Queue the batch

The call returns 202 immediately with a codeBatch to track. The Idempotency-Key makes retries safe — replays return the original batch instead of queuing a second one.

curl -X POST https://api.pixoo.app/api/v1/discounts/1234567890/code-batches \
  -H "Authorization: Bearer pixoo_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: summer-launch-2026" \
  -d '{"quantity": 10000, "prefix": "SUMMER-"}'
202 — response.json (grab codeBatch.id for step 2)
{
  "codeBatch": {
    "id": "cb_9b1deb4d…",
    "status": "queued",
    "totalCodes": 10000,
    "codesProcessed": 0,
    "percentComplete": 0,
    "queuePosition": 0,
    "discountId": "1234567890",
    "createdAt": "2026-07-10T09:00:00Z",
    "completedAt": null,
    "error": null
  }
}

Wait for it — webhook or poll

Subscribe to code_batch.completed (see the webhook guide) and Pixoo pings you the moment the batch lands. Or poll the batch — once completed, add ?include=codes to fetch every code in one go.

curl "https://api.pixoo.app/api/v1/code-batches/cb_9b1deb4d…?include=codes" \
  -H "Authorization: Bearer pixoo_live_YOUR_KEY"
200 — response.json
{
  "codeBatch": {
    "id": "cb_9b1deb4d…",
    "status": "completed",
    "totalCodes": 10000,
    "codesProcessed": 10000,
    "percentComplete": 100,
    "queuePosition": null,
    "discountId": "1234567890",
    "createdAt": "2026-07-10T09:00:00Z",
    "completedAt": "2026-07-10T09:04:12Z",
    "error": null,
    "codes": ["SUMMER-7F3K9Q2M8T1Z", "SUMMER-2B8XW4KQ90MJ", "…"]
  }
}

Distribute

The codes are yours: pipe them to your ESP, print partner, loyalty platform or affiliate portal. Each code is unique on the discount, and the discount enforces all the usual Pixoo rules — markets, currencies, caps — at checkout.

Guide 02

Receive & verify webhooks

Three events can reach your server: code.redeemed, code_batch.completed and gift.redeemed. Every delivery is signed — always verify before trusting the payload.

Subscribe

The response contains the signingSecret (whsec_…) once. Store it — it signs every delivery and is never shown again. Up to 10 active webhooks per shop.

curl -X POST https://api.pixoo.app/api/v1/webhooks \
  -H "Authorization: Bearer pixoo_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event": "code.redeemed", "targetUrl": "https://example.com/hooks/pixoo"}'
201 — response.json
{
  "webhook": {
    "id": "…",
    "event": "code.redeemed",
    "targetUrl": "https://example.com/hooks/pixoo",
    "status": "active",
    "lastDeliveryAt": null,
    "createdAt": "2026-07-10T13:00:00Z"
  },
  "signingSecret": "whsec_…"
}

Verify the signature

Each delivery carries Pixoo-Signature: sha256=<hex HMAC-SHA256(raw body, secret)>. A complete Express server, ready to run:

server.js — complete webhook consumer
import express from 'express'
import crypto from 'node:crypto'

const app = express()

// The signing secret returned once by POST /webhooks.
const SIGNING_SECRET = process.env.PIXOO_WEBHOOK_SECRET // "whsec_…"

const seenEventIds = new Set() // use your database in production

// Keep the raw body: the signature covers the exact bytes Pixoo sent.
app.post(
  '/hooks/pixoo',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    // Pixoo-Signature: sha256=<hex HMAC-SHA256(raw body, signing secret)>
    const signature = req.get('Pixoo-Signature') ?? ''
    const expected =
      'sha256=' +
      crypto.createHmac('sha256', SIGNING_SECRET).update(req.body).digest('hex')

    const valid =
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
    if (!valid) return res.status(401).send('invalid signature')

    // Deliveries are at-least-once — dedupe on Pixoo-Event-Id.
    const eventId = req.get('Pixoo-Event-Id')
    if (seenEventIds.has(eventId)) return res.sendStatus(200)
    seenEventIds.add(eventId)

    const { event, data } = JSON.parse(req.body)
    if (event === 'code.redeemed') {
      // data: { orderId, discountTitle, code, savingsAmount, currency }
      markCodeAsRedeemed(data.code) // your storage — feeds the purge cron
    }

    res.sendStatus(200) // answer 2xx within 10 seconds
  },
)

app.listen(3000)

Know the delivery rules

These are the payloads your endpoint receives — no customer PII, by design (no names, no emails). The body id is the same value as the Pixoo-Event-Id header. Note that code_batch.completed does not carry the codes themselves — fetch them with GET /code-batches/{batchId}?include=codes.

delivery headers
POST https://your-server.example/hooks/pixoo
Content-Type: application/json
Pixoo-Event: code.redeemed
Pixoo-Event-Id: evt_01H…   ← dedup on this
Pixoo-Signature: sha256=3f1a…
{
  "id": "evt_01H…",
  "event": "code.redeemed",
  "createdAt": "2026-07-10T14:02:11Z",
  "data": {
    "orderId": "…",
    "discountTitle": "Holiday bulk",
    "code": "VIP-7F3K9Q2M8T1Z",
    "savingsAmount": 9.5,
    "currency": "EUR"
  }
}
  • Deliveries are at-least-once — dedupe on the Pixoo-Event-Id header.
  • Answer 2xx within 10 seconds; do slow work asynchronously.
  • Failures retry with backoff: 1 min, 5 min, 30 min, 2 h, 8 h. Retries survive restarts.
  • After 20 consecutive terminally-failed deliveries the subscription is auto-disabled — recreate it to resume.
  • Missed something during an outage? GET /events replays the last 7 days.

Guide 03

Recycle redeemed codes on a schedule

Single-use codes pile up. A discount caps at 100,000 codes — so purge the redeemed ones nightly and the loop runs forever. Capacity is freed the moment a deletion is accepted.

Collect redeemed codes

Your webhook consumer from Guide 02 already marks codes as redeemed when code.redeemed fires. That list is the input for the cron below.

Purge them nightly

Deletions take up to 250 explicit codes per call and run one at a time per discount — the script waits for each to complete. Unknown codes are silently ignored, and cleanup works even on expired discounts.

// Nightly cron: purge redeemed codes so the discount
// never reaches the 100,000-code cap.
import crypto from 'node:crypto'

const BASE = 'https://api.pixoo.app/api/v1'
const HEADERS = { Authorization: `Bearer ${process.env.PIXOO_API_KEY}` }
const DISCOUNT_ID = '1234567890'

// 1. Codes your webhook consumer marked as redeemed
const redeemed = await getRedeemedCodesFromDb() // your storage

// 2. Delete in chunks of 250 (the per-call maximum).
//    One deletion runs at a time per discount, so wait between chunks.
for (let i = 0; i < redeemed.length; i += 250) {
  const chunk = redeemed.slice(i, i + 250)
  const res = await fetch(`${BASE}/discounts/${DISCOUNT_ID}/code-deletions`, {
    method: 'POST',
    headers: {
      ...HEADERS,
      'Content-Type': 'application/json',
      // Same key when a chunk is retried — that is what prevents over-release
      'Idempotency-Key': `purge-${DISCOUNT_ID}-${chunk[0]}`,
    },
    body: JSON.stringify({ codes: chunk }),
  })
  let { codeDeletion } = await res.json() // 202 — accepted
  console.log(`purging ${codeDeletion.matchedCount}/${chunk.length} codes`)

  while (codeDeletion.status === 'processing') {
    await new Promise((resolve) => setTimeout(resolve, 2000))
    const poll = await fetch(`${BASE}/code-deletions/${codeDeletion.id}`, {
      headers: HEADERS,
    })
    ;({ codeDeletion } = await poll.json())
  }
}
202 — response.json
{
  "codeDeletion": {
    "id": "cd_01H…",
    "status": "processing",
    "requestedCount": 250,
    "matchedCount": 248,
    "discountId": "1234567890",
    "createdAt": "2026-07-10T09:00:00Z",
    "completedAt": null,
    "error": null
  }
}

API reference

All 14 endpoints

Scannable first, detailed on demand — expand any card for parameters, response shapes and error codes.

Keys

Check that a key is wired correctly before anything else.

scope: any valid key

Works with any valid key, no specific scope — the “is my key wired correctly?” call.

Response200

response.json
{
  "shop": "acme.myshopify.com",
  "key": {
    "label": "Production backend",
    "lastFour": "8T1Z",
    "scopes": ["discounts:read", "discounts:write"],
    "createdAt": "2026-07-10T09:00:00Z",
    "lastUsedAt": "2026-07-10T10:00:00Z"
  },
  "rateLimit": {
    "sustained": { "limit": 120, "windowSeconds": 60 },
    "burst": { "limit": 20, "windowSeconds": 1 }
  }
}

Errors

unauthorizedrate_limited

Discounts

List, inspect and create Pixoo discounts of any of the four types.

scope: discounts:read

discountClasses is always an array (a discount may carry several classes) — never assume a single class. A page may contain fewer than limit items while nextCursor is set: iterate until nextCursor is null.

Parameters

statusquery
active | scheduled | expired
titlequery
Exact title match (the Flow-style lookup)
isBulkquery
true | false
cursorquery
From the previous page
limitquery
1–50, default 20

Response200

response.json
{
  "discounts": [
    {
      "id": "1234567890",
      "title": "Holiday bulk",
      "status": "active",
      "discountClasses": ["PRODUCT"],
      "isBulk": true,
      "codesCount": 48210,
      "createdAt": "2026-06-01T09:00:00Z"
    }
  ],
  "nextCursor": "eyJ…"
}

Errors

rate_limited
scope: discounts:writesupports Idempotency-Key

Creates any of the four discount types (product, order, shipping, buy-x-get-y), validated by the same server-side chain as the CSV import. The body uses the exact field names of the import field reference (Pixoo admin → Discounts → Import → Field reference), with native JSON types. Unknown fields are rejected by name. method: "Bulk" additionally requires bulkConfig — the discount is created with its first code and the remaining codes ride the async queue; the response then includes a codeBatch to poll.

Parameters

typebody
product-discount | order-discount | shipping-discount | buy-x-get-y-discount (required)
methodbody
Code | Automatic | Bulk (required)
titlebody
Required
codebody
Required when method is "Code"
bulkConfigbody
Required when method is "Bulk": { numberOfCodes, codeLength?, prefix?, suffix?, characterSet?, specificCodes?, isSpecificCodes? }
…type-specific fieldsbody
Import field reference is the single source of truth for names, enums and defaults

Request body

POST /discounts
{
  "type": "product-discount",
  "method": "Bulk",
  "title": "Welcome codes",
  "discountValuesType": "PERCENTAGE",
  "discountPercentageValue": 15,
  "appliesToEligibility": "COLLECTIONS",
  "appliesToSelectedItems": [{ "id": "gid://shopify/Collection/123456789" }],
  "bulkConfig": { "numberOfCodes": 1000, "prefix": "WELCOME-" }
}

Response201

response.json
{
  "discount": {
    "id": "1234567890",
    "title": "Welcome codes",
    "status": "active",
    "discountClasses": ["PRODUCT"],
    "isBulk": true,
    "codesCount": 1,
    "createdAt": "2026-07-10T09:00:00Z"
  },
  "codeBatch": {
    "id": "cb_9b1deb4d…",
    "status": "queued",
    "totalCodes": 999,
    "codesProcessed": 0,
    "percentComplete": 0,
    "queuePosition": 0,
    "discountId": "1234567890",
    "createdAt": "2026-07-10T09:00:00Z",
    "completedAt": null,
    "error": null
  }
}

Errors

invalid_requestconflictrate_limited
scope: discounts:read

Same representation as a list item. Discounts are addressed by their numeric Shopify id (the one visible in the admin URL), serialized as a string.

Parameters

idpath
Numeric Shopify discount id, as a string

Response200

response.json
{
  "id": "1234567890",
  "title": "Holiday bulk",
  "status": "active",
  "discountClasses": ["PRODUCT"],
  "isBulk": true,
  "codesCount": 48210,
  "createdAt": "2026-06-01T09:00:00Z"
}

Errors

not_foundrate_limited

Codes

Mint codes one at a time, in 100k batches, and clean them up.

scope: discounts:read

Returns invalid_config for automatic discounts — they have no codes.

Parameters

idpath
Discount id
searchquery
Matches code values
cursorquery
From the previous page
limitquery
1–100, default 20

Response200

response.json
{
  "codes": [{ "code": "VIP-7F3K9Q2M8T1Z", "usageCount": 1 }],
  "nextCursor": null
}

Errors

not_foundinvalid_configrate_limited
scope: discounts:writesupports Idempotency-Key

Works on Pixoo bulk discounts. The sweet spot for “one fresh code per customer” flows: no polling, the codes are in the response body. Above 10 codes, use code-batches.

Parameters

idpath
Discount id (must be a Pixoo bulk discount)
quantitybody
Integer 1–10, default 1
prefixbody
String ≤32 chars, default "" — prepended to each code

Request body

POST /discounts/{id}/codes
{ "quantity": 1, "prefix": "XMAS-" }

Response200

response.json
{
  "codes": ["XMAS-7F3K9Q2M8T1Z"],
  "discount": { "id": "1234567890", "title": "Holiday bulk" }
}

Errors

not_foundinvalid_configcap_reachedconflictinvalid_requestrate_limited
scope: discounts:writesupports Idempotency-Key

Returns 202 immediately with a codeBatch to track. Get notified with the code_batch.completed webhook, or poll GET /code-batches/{id}. quantity is capped by the remaining discount capacity.

Parameters

idpath
Discount id
quantitybody
Integer 1–100,000 (required)
prefixbody
String ≤32 chars, optional

Request body

POST /discounts/{id}/code-batches
{ "quantity": 50000, "prefix": "SUMMER-" }

Response202

response.json
{
  "codeBatch": {
    "id": "cb_9b1deb4d…",
    "status": "queued",
    "totalCodes": 50000,
    "codesProcessed": 0,
    "percentComplete": 0,
    "queuePosition": 0,
    "discountId": "1234567890",
    "createdAt": "2026-07-09T10:00:00Z",
    "completedAt": null,
    "error": null
  }
}

Errors

not_foundconflictinvalid_requestrate_limited
scope: discounts:read

status ∈ queued | processing | completed | failed. Adds percentComplete, queuePosition (while queued), completedAt, and error ({message, code}, failed only). With ?include=codes on a completed batch, the response adds the full codes array — large, up to 100k strings.

Parameters

idpath
Batch id from the 202 response
includequery
"codes" adds the full codes array on completed batches

Response200 — with ?include=codes

response.json
{
  "codeBatch": {
    "id": "cb_9b1deb4d…",
    "status": "completed",
    "totalCodes": 50000,
    "codesProcessed": 50000,
    "percentComplete": 100,
    "queuePosition": null,
    "discountId": "1234567890",
    "createdAt": "2026-07-09T10:00:00Z",
    "completedAt": "2026-07-09T10:18:27Z",
    "error": null,
    "codes": ["SUMMER-7F3K9Q2M8T1Z", "…"]
  }
}

Errors

not_foundrate_limited
scope: discounts:writesupports Idempotency-Key

Collect redeemed codes from code.redeemed webhooks, purge them here — the discount never reaches the 100,000-code ceiling. Codes are matched case-insensitively; codes that don’t exist on the discount are silently ignored (matchedCount tells you how many will be deleted). Works on expired discounts — cleanup is always legal. One deletion at a time per discount: a POST while one is processing returns 409 conflict. Use Idempotency-Key on retries — replaying without it can over-release capacity accounting.

Parameters

idpath
Discount id
codesbody
Array of 1–250 explicit code strings (required)

Request body

POST /discounts/{id}/code-deletions
{ "codes": ["VIP-7F3K9Q2M8T1Z", "VIP-2B8XW4KQ90MJ"] }

Response202

response.json
{
  "codeDeletion": {
    "id": "…",
    "status": "processing",
    "requestedCount": 250,
    "matchedCount": 248,
    "discountId": "1234567890",
    "createdAt": "…",
    "completedAt": null,
    "error": null
  }
}

Errors

not_foundinvalid_requestconflictrate_limited
scope: discounts:read

status ∈ processing | completed | failed (failed is reserved — current failures surface as request errors). Capacity accounting is freed as soon as the deletion is accepted.

Parameters

idpath
Deletion id from the 202 response

Response200

response.json
{
  "codeDeletion": {
    "id": "…",
    "status": "completed",
    "requestedCount": 250,
    "matchedCount": 248,
    "discountId": "1234567890",
    "createdAt": "2026-07-10T02:00:00Z",
    "completedAt": "2026-07-10T02:00:41Z",
    "error": null
  }
}

Errors

not_foundrate_limited

Webhooks & events

Let Pixoo call you — with a signed, retried, at-least-once delivery.

scope: discounts:write

event ∈ code.redeemed | code_batch.completed | gift.redeemed. targetUrl must be an https URL on a publicly reachable host. Up to 10 active webhooks per shop. The signingSecret is shown once, store it now; it signs every delivery. Re-subscribing the same event and URL is idempotent: it returns the existing subscription with a 200 and a null signingSecret, so an automation can safely re-subscribe.

Parameters

eventbody
code.redeemed | code_batch.completed | gift.redeemed (required)
targetUrlbody
Public https:// URL to deliver to — localhost/private/loopback addresses are rejected (required)

Request body

POST /webhooks
{ "event": "code.redeemed", "targetUrl": "https://example.com/hooks/pixoo" }

Response201 / 200

response.json
{
  "webhook": {
    "id": "…",
    "event": "code.redeemed",
    "targetUrl": "https://example.com/hooks/pixoo",
    "status": "active",
    "lastDeliveryAt": null,
    "createdAt": "2026-07-10T13:00:00Z"
  },
  "signingSecret": "whsec_…"
}

Errors

invalid_requestrate_limited
scope: discounts:read

The list never exposes signing secrets.

Response200

response.json
{
  "webhooks": [
    {
      "id": "…",
      "event": "code.redeemed",
      "targetUrl": "https://example.com/hooks/pixoo",
      "status": "active",
      "lastDeliveryAt": "2026-07-10T14:02:11Z",
      "createdAt": "2026-07-10T13:00:00Z"
    }
  ]
}

Errors

rate_limited
scope: discounts:write

Auto-disabled subscriptions (after 20 consecutive terminally-failed deliveries) must be recreated to resume.

Parameters

idpath
Webhook id

Response200

response.json
{ "deleted": true }

Errors

not_foundrate_limited
scope: discounts:read

Newest first, 7-day window. Items have the exact delivery shape (id, event, createdAt, data). Payloads carry no customer PII.

Parameters

typequery
Event name filter
cursorquery
From the previous page
limitquery
1–100, default 20

Response200

response.json
{
  "events": [
    {
      "id": "evt_…",
      "event": "code.redeemed",
      "createdAt": "2026-07-10T14:02:11Z",
      "data": {
        "orderId": "…",
        "discountTitle": "Holiday bulk",
        "code": "VIP-7F3K9Q2M8T1Z",
        "savingsAmount": 9.5,
        "currency": "EUR"
      }
    }
  ],
  "nextCursor": null
}

Errors

invalid_requestrate_limited

Versioning

Built to not break you

  • The version lives in the URL: /v1.
  • Additive changes (new optional fields, new endpoints, new event types) may happen at any time without notice — consumers must ignore unknown fields.
  • Breaking changes only ship as /v2, with at least 12 months of continued /v1 support.
  • During the private beta, field names may still shift — the contract freezes the day the first external key is issued.