Stow Cards Wallet passes

Developers

Webhooks

Signed, retried HTTP callbacks for everything that happens on your loyalty programs: enrollments, points, redemptions and wallet lifecycle. Full data schemas for every event also live in the API reference under the OpenAPI webhooks section.

Receiving webhooks

Create a subscription with POST /merchants/me/webhooks, pointing at an HTTPS endpoint you control. Stow Cards POSTs one JSON envelope per event, with these headers:

HeaderValue
X-Stow-Webhook-Eventthe event type, e.g. points.changed
X-Stow-Webhook-TimestampISO-8601 timestamp of the delivery attempt
X-Stow-Webhook-Subscriptionthe subscription id
X-Stow-Webhook-Signaturesha256= + HMAC-SHA256 signature (present when the subscription has a secret)
User-Agentstow-cards-webhook/1.0

Respond with any 2xx within the subscription's timeoutMs. Non-2xx or timeouts are retried up to retryPolicy.maxAttempts with backoffMs between attempts. Deliveries (payload, status, attempts) are inspectable at GET /merchants/me/webhooks/delivery-logs, and any logged delivery can be re-sent with POST /merchants/me/webhooks/delivery-logs/{logId}/replay.

Idempotency on your side: deliveries are at-least-once. Dedupe on eventId: replays and retries reuse it.

Verifying the signature

The signature is HMAC-SHA256(secret, "{timestamp}.{rawBody}"), hex-encoded, prefixed with sha256=. The {timestamp} is the value of the X-Stow-Webhook-Timestamp header. Checking it also protects you from replays (reject deliveries older than, say, 5 minutes).

Node.js

import crypto from 'node:crypto';

function verifyStowWebhook(req, secret, toleranceMs = 5 * 60 * 1000) {
  const timestamp = req.headers['x-stow-webhook-timestamp'];
  const received = req.headers['x-stow-webhook-signature'] || '';
  if (!timestamp || !received.startsWith('sha256=')) return false;
  if (Math.abs(Date.now() - Date.parse(timestamp)) > toleranceMs) return false;

  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${req.rawBody}`)   // rawBody: the exact bytes received
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}

Python

import hashlib, hmac, time
from datetime import datetime, timezone

def verify_stow_webhook(headers, raw_body: bytes, secret: str, tolerance_s=300):
    timestamp = headers.get("X-Stow-Webhook-Timestamp", "")
    received = headers.get("X-Stow-Webhook-Signature", "")
    if not timestamp or not received.startswith("sha256="):
        return False
    sent = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
    if abs(time.time() - sent.timestamp()) > tolerance_s:
        return False
    expected = "sha256=" + hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(received, expected)

Compute the HMAC over the raw request bytes, not a re-serialized JSON object, because key order matters.

Choosing events

Subscribe to specific events or * for everything. Test any subscription with POST /merchants/me/webhooks/{id}/test. It queues a real delivery of a test: true payload using the subscription's first event type.

Event catalog

#

member.created

A member enrolled (any channel)

Fired by enrollment for both public-page and API enrollments.

data payload

PropertyTypeDescription
memberIdstring
memberCodestring
programIdstring
merchantIdstring
emailstring
firstNamestring
lastNamestring
sourcestring

Enrollment channel (public page, console, API, import).

attributionobject
passIssuanceobject
properties
PropertyTypeDescription
appleboolean
googleboolean

Delivery payload

{
  "specversion": "1.0",
  "id": "string",
  "type": "cards.stow.member.created",
  "time": "2026-07-02T10:00:00.000Z",
  "event": "member.created",
  "eventId": "string",
  "occurredAt": "2026-07-02T10:00:00.000Z",
  "tenantId": "string",
  "sourceService": "string",
  "data": {
    "memberId": "string",
    "memberCode": "string",
    "programId": "string",
    "merchantId": "string",
    "email": "string",
    "firstName": "string",
    "lastName": "string",
    "source": "string",
    "attribution": {},
    "passIssuance": {
      "apple": true,
      "google": true
    }
  }
}
#

member.updated

Member fields changed

data payload

object

Event-specific payload (see per-event schemas).

Delivery payload

{
  "specversion": "1.0",
  "id": "string",
  "type": "cards.stow.member.updated",
  "time": "2026-07-02T10:00:00.000Z",
  "event": "member.updated",
  "eventId": "string",
  "occurredAt": "2026-07-02T10:00:00.000Z",
  "tenantId": "string",
  "sourceService": "string",
  "data": {}
}
#

points.changed

Points balance changed

Fired on every balance adjustment (console, API, reader scan).

data payload

PropertyTypeDescription
memberIdstring
memberCodestring
programIdstring
merchantIdstring
fieldstring

The fieldValues key that changed.

previousinteger
currentinteger
deltainteger
tierstring

Tiered programs only.

previousTierstring
tierUpgradedboolean
changeMessagestring
passUpdateobject | null

What happened to the wallet pass after the change. null when the pass service did not confirm the update; the balance change itself is already saved.

properties
PropertyTypeDescription
serialNumberstring

The pass serial (a UUID). Not the Member ID.

lastUpdateTagstring

Timestamp of the latest pass version. Apple devices compare it to decide whether to fetch the pass again.

rebuiltboolean

True when the signed pass file was rebuilt (real mode only).

appleobject
properties
PropertyTypeDescription
devicesNotifiedinteger

Apple devices that accepted the push.

devicesAttemptedinteger

Apple devices registered for this pass.

modeenum

stub when Apple credentials are missing or do not match, or when the program's push messages are off.

one of real, stub
googleobject
properties
PropertyTypeDescription
objectPatchedboolean

True when the Google Wallet pass was updated. Only a pass the customer saved to Google Wallet is updated, and only in real mode.

modeenum
one of real, stub

Delivery payload

{
  "specversion": "1.0",
  "id": "string",
  "type": "cards.stow.points.changed",
  "time": "2026-07-02T10:00:00.000Z",
  "event": "points.changed",
  "eventId": "string",
  "occurredAt": "2026-07-02T10:00:00.000Z",
  "tenantId": "string",
  "sourceService": "string",
  "data": {
    "memberId": "string",
    "memberCode": "string",
    "programId": "string",
    "merchantId": "string",
    "field": "string",
    "previous": 0,
    "current": 0,
    "delta": 0,
    "tier": "string",
    "previousTier": "string",
    "tierUpgraded": true,
    "changeMessage": "string",
    "passUpdate": {
      "serialNumber": "string",
      "lastUpdateTag": "string",
      "rebuilt": true,
      "apple": {
        "devicesNotified": 0,
        "devicesAttempted": 0,
        "mode": "real"
      },
      "google": {
        "objectPatched": true,
        "mode": "real"
      }
    }
  }
}
#

reward.redeemed

A reward was redeemed

data payload

PropertyTypeDescription
memberIdstring
memberCodestring
programIdstring
merchantIdstring
fieldstring
previousinteger
currentinteger

Post-redeem balance (0 for stamp resets).

goalinteger
changeMessagestring
passUpdateobject | null

What happened to the wallet pass after the change. null when the pass service did not confirm the update; the balance change itself is already saved.

properties
PropertyTypeDescription
serialNumberstring

The pass serial (a UUID). Not the Member ID.

lastUpdateTagstring

Timestamp of the latest pass version. Apple devices compare it to decide whether to fetch the pass again.

rebuiltboolean

True when the signed pass file was rebuilt (real mode only).

appleobject
properties
PropertyTypeDescription
devicesNotifiedinteger

Apple devices that accepted the push.

devicesAttemptedinteger

Apple devices registered for this pass.

modeenum

stub when Apple credentials are missing or do not match, or when the program's push messages are off.

one of real, stub
googleobject
properties
PropertyTypeDescription
objectPatchedboolean

True when the Google Wallet pass was updated. Only a pass the customer saved to Google Wallet is updated, and only in real mode.

modeenum
one of real, stub

Delivery payload

{
  "specversion": "1.0",
  "id": "string",
  "type": "cards.stow.reward.redeemed",
  "time": "2026-07-02T10:00:00.000Z",
  "event": "reward.redeemed",
  "eventId": "string",
  "occurredAt": "2026-07-02T10:00:00.000Z",
  "tenantId": "string",
  "sourceService": "string",
  "data": {
    "memberId": "string",
    "memberCode": "string",
    "programId": "string",
    "merchantId": "string",
    "field": "string",
    "previous": 0,
    "current": 0,
    "goal": 0,
    "changeMessage": "string",
    "passUpdate": {
      "serialNumber": "string",
      "lastUpdateTag": "string",
      "rebuilt": true,
      "apple": {
        "devicesNotified": 0,
        "devicesAttempted": 0,
        "mode": "real"
      },
      "google": {
        "objectPatched": true,
        "mode": "real"
      }
    }
  }
}
#

pass.created

A wallet pass was built

data payload

object

Event-specific payload (see per-event schemas).

Delivery payload

{
  "specversion": "1.0",
  "id": "string",
  "type": "cards.stow.pass.created",
  "time": "2026-07-02T10:00:00.000Z",
  "event": "pass.created",
  "eventId": "string",
  "occurredAt": "2026-07-02T10:00:00.000Z",
  "tenantId": "string",
  "sourceService": "string",
  "data": {}
}
#

pass.installed

Pass added to a wallet

Fired by the wallet web service when a device registers the pass.

data payload

PropertyTypeDescription
memberIdstring
memberCodestring
serialNumberstring
programIdstring
platformenum
one of apple, google

Delivery payload

{
  "specversion": "1.0",
  "id": "string",
  "type": "cards.stow.pass.installed",
  "time": "2026-07-02T10:00:00.000Z",
  "event": "pass.installed",
  "eventId": "string",
  "occurredAt": "2026-07-02T10:00:00.000Z",
  "tenantId": "string",
  "sourceService": "string",
  "data": {
    "memberId": "string",
    "memberCode": "string",
    "serialNumber": "string",
    "programId": "string",
    "platform": "apple"
  }
}
#

pass.updated

Pass content pushed/refreshed

data payload

PropertyTypeDescription
memberIdstring
memberCodestring
serialNumberstring
programIdstring
platformenum
one of apple, google

Delivery payload

{
  "specversion": "1.0",
  "id": "string",
  "type": "cards.stow.pass.updated",
  "time": "2026-07-02T10:00:00.000Z",
  "event": "pass.updated",
  "eventId": "string",
  "occurredAt": "2026-07-02T10:00:00.000Z",
  "tenantId": "string",
  "sourceService": "string",
  "data": {
    "memberId": "string",
    "memberCode": "string",
    "serialNumber": "string",
    "programId": "string",
    "platform": "apple"
  }
}
#

pass.removed

Pass removed from a wallet

data payload

PropertyTypeDescription
memberIdstring
memberCodestring
serialNumberstring
programIdstring
platformenum
one of apple, google

Delivery payload

{
  "specversion": "1.0",
  "id": "string",
  "type": "cards.stow.pass.removed",
  "time": "2026-07-02T10:00:00.000Z",
  "event": "pass.removed",
  "eventId": "string",
  "occurredAt": "2026-07-02T10:00:00.000Z",
  "tenantId": "string",
  "sourceService": "string",
  "data": {
    "memberId": "string",
    "memberCode": "string",
    "serialNumber": "string",
    "programId": "string",
    "platform": "apple"
  }
}
#

pass.scanned

Pass scanned at the till

data payload

object

Event-specific payload (see per-event schemas).

Delivery payload

{
  "specversion": "1.0",
  "id": "string",
  "type": "cards.stow.pass.scanned",
  "time": "2026-07-02T10:00:00.000Z",
  "event": "pass.scanned",
  "eventId": "string",
  "occurredAt": "2026-07-02T10:00:00.000Z",
  "tenantId": "string",
  "sourceService": "string",
  "data": {}
}
#

campaign.sent

A campaign finished sending

data payload

object

Event-specific payload (see per-event schemas).

Delivery payload

{
  "specversion": "1.0",
  "id": "string",
  "type": "cards.stow.campaign.sent",
  "time": "2026-07-02T10:00:00.000Z",
  "event": "campaign.sent",
  "eventId": "string",
  "occurredAt": "2026-07-02T10:00:00.000Z",
  "tenantId": "string",
  "sourceService": "string",
  "data": {}
}
#

batch.completed

A batch job completed

data payload

object

Event-specific payload (see per-event schemas).

Delivery payload

{
  "specversion": "1.0",
  "id": "string",
  "type": "cards.stow.batch.completed",
  "time": "2026-07-02T10:00:00.000Z",
  "event": "batch.completed",
  "eventId": "string",
  "occurredAt": "2026-07-02T10:00:00.000Z",
  "tenantId": "string",
  "sourceService": "string",
  "data": {}
}
#

batch.failed

A batch job failed

data payload

object

Event-specific payload (see per-event schemas).

Delivery payload

{
  "specversion": "1.0",
  "id": "string",
  "type": "cards.stow.batch.failed",
  "time": "2026-07-02T10:00:00.000Z",
  "event": "batch.failed",
  "eventId": "string",
  "occurredAt": "2026-07-02T10:00:00.000Z",
  "tenantId": "string",
  "sourceService": "string",
  "data": {}
}