~netlandish/ponch-api-docs

#Events

Ponch records an event whenever something happens to a card. You can receive events as webhooks the moment they happen, or fetch them from the events feed. Both give you the same payload for the same event.

Event Fires when id is Webhook Feed
customer.enrolled A customer adds a new card. Re-downloading an existing card doesn't count the card's id ✅ ✅
stamp.added A card is stamped, or its count is set the stamp's id ✅ ✅
reward.earned A card reaches its target. One event per reward the reward's id ✅ ✅
reward.redeemed A reward is redeemed. One event per reward the reward's id ✅ ✅
card.voided A merchant deactivates a card <card id>:<number> ✅ ✅
card.unvoided A deactivated card is restored <card id>:<number> ✅ ❌

card.unvoided is webhook-only.

#Shared fields

Every event payload has these top-level keys:

Key
id What happened. Use this to deduplicate. It's the same on every delivery of the same event, including retries and replays
occurred_at When it happened, ISO-8601. The feed is sorted by this
event The event type, e.g. stamp.added
event_id On a webhook, identifies this event's delivery to this webhook: retries and replays repeat it, but two webhooks get different values. On the feed, the same as id. Deduplicate on id, not on this
company {id, name, slug}
store {id, name, slug}: for stamp and reward events, the store where it happened when known; otherwise the template's store. null for a company-wide template with no store recorded
template {id, slug, name}
customer {id, email, phone, display_name}. Empty strings for details the customer didn't give
card The card's state, below
"card": {
  "id": "2b4d6f8a-0c2e-4a6c-8e0a-2c4e6a8c0e4d",
  "created_at": "2026-07-29T13:53:53.421230+00:00",
  "barcode_value": "SC-1540CD2A802A",
  "current_stamps": 0,
  "total_stamps_required": 8,
  "total_stamps_earned": 8,
  "completions": 1,
  "pending_rewards": 1,
  "status": "active"
}

The fields mean the same as on the card object.

card.voided and card.unvoided need a composite id. One card can be deactivated, restored and deactivated again, and with a plain card id the second deactivation would look like a duplicate of the first. Treat the id as an opaque string; don't parse it.

occurred_at is when the event itself happened. For reward.earned that's when the reward was earned; for reward.redeemed on the same reward it's when it was redeemed.

#customer.enrolled

Only the shared fields. Here id equals card.id.

{
  "id": "2b4d6f8a-0c2e-4a6c-8e0a-2c4e6a8c0e4d",
  "event": "customer.enrolled",
  "event_id": "71c3e5a7-9b1d-4f3a-8c5e-7a9b1d3f5c6e",
  "occurred_at": "2026-07-29T13:53:53.421230+00:00",
  "company":  {"id": "3f1c2a9e-5b7d-4c11-9a2e-6d0b8f4e7a21", "name": "Luna Coffee", "slug": "luna-coffee"},
  "store":    {"id": "8a4e6c2b-1d3f-4e5a-b7c9-0f2e4d6a8b13", "name": "Downtown", "slug": "downtown"},
  "template": {"id": "c7d9e1f3-2a4b-4c6d-8e0f-1a3b5c7d9e2f", "slug": "coffee-card", "name": "Coffee Card"},
  "customer": {
    "id": "5e7f9a1b-3c5d-4e7f-9a1b-3c5d7e9f1a2b",
    "email": "ana@example.com",
    "phone": "+15551234567",
    "display_name": "Ana"
  },
  "card": {
    "id": "2b4d6f8a-0c2e-4a6c-8e0a-2c4e6a8c0e4d",
    "created_at": "2026-07-29T13:53:53.421230+00:00",
    "barcode_value": "SC-1540CD2A802A",
    "current_stamps": 0,
    "total_stamps_required": 8,
    "total_stamps_earned": 0,
    "completions": 0,
    "pending_rewards": 0,
    "status": "active"
  }
}

#stamp.added

Adds stamp:

"stamp": {
  "id": "9c1e3a5b-7d9f-4b1d-a3f5-7b9d1f3a5c7e",
  "stamps_added": 1,
  "stamps_before": 7,
  "stamps_after": 0,
  "source": "api",
  "reward_earned": true,
  "rewards_earned": 1,
  "created_at": "2026-08-11T13:27:59.253264+00:00"
}
  • stamps_added can be negative when a set-count lowered the card. source is then adjust.
  • stamps_after wraps on a repeating card, so in the example the 8th stamp takes it to 0. For a counter that only goes up, use card.total_stamps_earned.
  • source is scan, manual, api, bonus or adjust. More values may be added.
  • rewards_earned can be more than 1 for a bulk stamp. A matching number of reward.earned events fire alongside it.

#reward.earned

Adds reward:

"reward": {
  "id": "e4a6c8e0-2b4d-4f6a-8c0e-2a4c6e8a0c3b",
  "completion_number": 1,
  "status": "pending",
  "description": "Free coffee",
  "earned_at": "2026-08-11T13:27:59.253264+00:00",
  "redeemed_at": null,
  "stamp_event_id": "9c1e3a5b-7d9f-4b1d-a3f5-7b9d1f3a5c7e"
}

stamp_event_id is the stamp.id of the stamp that earned it. It can be null on some older rewards. completion_number is 1 for the card's first reward, 2 for its second, and so on. description is the merchant's reward text, as written.

#reward.redeemed

Adds reward, as above, with status: "redeemed", redeemed_at set, and one more key:

"redeemed_by": {"id": "0d2f4b6d-8f0b-4d2f-a4b6-d8f0b2d4f6a9", "name": "leo"}

redeemed_by is the staff member who redeemed it (name is their username), or null when the reward was redeemed through the API.

#card.voided and card.unvoided

Add reason, the merchant's note on why (may be empty):

"reason": "Card reported lost"

card.status is voided on card.voided. On card.unvoided it's back to active, or completed for a one-time card that still has a reward waiting. Rewards that were pending when the card was deactivated are pending again, so check card.pending_rewards.

#Delivery is at least once

The same event can reach you more than once: a retry after a timeout, a manual replay, or the feed returning an item you've already processed. Store the id of each event you handle and skip any you've seen.

About this wiki

commit 5e140ef5493db7e98830f4b7ef4d620de5419518
Author: Peter Sanchez <peter@netlandish.com>
Date:   2026-10-20T06:45:49-06:00

add proper title and descriptions to each page
Clone this wiki ("main" branch)
https://git.code.netlandish.com/~netlandish/ponch-api-docs (read-only)
git@git.code.netlandish.com:~netlandish/ponch-api-docs (read/write)