~netlandish/ponch-api-docs

#Endpoints

These are all the endpoints an API key can call. Paths are relative to https://app.getponch.com/api. Every request needs the headers, and every endpoint can return 401 for a bad key and 422 for an invalid request, so those aren't repeated below.

Endpoints marked ✎ changes data act on real cards. A change updates the customer's wallet pass and fires webhooks, exactly as if staff had done it at the counter.

Endpoint Purpose
GET /integrations/me Check a key; label a connection
✎ POST /stamping/stamp Add stamps to a card
✎ POST /stamping/redeem Redeem pending rewards
✎ POST /stamping/set-count Set a card's stamp count
POST /stamping/lookup Get a card by barcode; 404 if none
GET /integrations/cards/search Get a card by barcode; empty list if none
GET /stamp-cards/{card_id}/rewards A card's reward history
GET /integrations/customers/search Find customers and their cards
GET /integrations/templates List templates (stamp card designs)
GET /stores/ List stores
GET /cards/{slug}/enrollment-url A template's join link / QR payload
/integrations/webhooks… Create, list, update and delete webhooks (7 endpoints)
GET /integrations/events The events feed

#Identity

#GET /integrations/me

Who the key belongs to. It's quick, so use it to test a key when a connection is set up, and to label the connection (e.g. "Luna Coffee").

{
  "company": {"id": "3f1c2a9e-5b7d-4c11-9a2e-6d0b8f4e7a21", "name": "Luna Coffee", "slug": "luna-coffee"},
  "merchant": {"id": "6a8c0e2a-4c6e-4a8c-b0e2-4a6c8e0a2c5d", "slug": "luna-coffee"},
  "plan": {"slug": "growth", "name": "Growth"},
  "api_enabled": true,
  "store_count": 1,
  "active_template_count": 1
}

merchant is the Ponch account that owns the company. One account can own several companies.


#Cards and stamping

#The card object

Most card endpoints return this:

{
  "id": "2b4d6f8a-0c2e-4a6c-8e0a-2c4e6a8c0e4d",
  "barcode_value": "SC-1540CD2A802A",
  "current_stamps": 3,
  "total_stamps_required": 8,
  "total_stamps_earned": 11,
  "completions": 1,
  "pending_rewards": 1,
  "status": "active",
  "reward_description": "Free coffee",
  "template_name": "Coffee Card",
  "customer_display": "Ana",
  "created_at": "2026-07-29T13:53:53.421Z"
}
Field
current_stamps Stamps toward the next reward. On a repeating card it wraps when the target is reached (the extra stamps carry over); on a one-time card it stops at the target
total_stamps_required Stamps needed for a reward
total_stamps_earned Every stamp the card has ever earned. Only goes up, so use this as a counter
completions How many times the card has reached the target
pending_rewards Rewards earned and not yet redeemed. Check this before redeeming
status active, completed, redeemed, expired or voided (shown as Deactivated in the dashboard). Only active cards can be stamped
customer_display The customer's name; failing that their email, then phone, then the start of their ID

Repeating vs. one-time cards. Most templates repeat: after a reward the card starts over and keeps collecting. A template can also be set up as a one-time offer. Then the card's status becomes completed when it reaches the target, and it can't be stamped again (its reward can still be redeemed).

#POST /stamping/stamp

✎ Changes data.

Add one or more stamps to a card. If the card reaches its target, a reward is added to its pending queue. On a repeating card, a bulk stamp can cross the target more than once and earn several rewards.

Body field Type
barcode_value string Required. The card's SC-… barcode, or the scanned QR string
stamps integer 1–100, default 1. Capped by the template's stamps-per-visit setting
source string api (recommended), scan, manual or bonus. Default scan. bonus is for goodwill stamps; it doesn't count as the customer's first visit for referrals
notes string Free text, visible to the merchant
idempotency_key string Strongly recommended; required when stamps > 1. See Idempotency
{"barcode_value": "SC-1540CD2A802A", "source": "api", "idempotency_key": "order-10482"}

Response 200:

{
  "card": { "...": "the card object, after the stamp" },
  "reward_earned": true,
  "rewards_earned": 1,
  "message": "Reward earned: Free coffee"
}

reward_earned is true if this call earned at least one reward, and rewards_earned says how many. message is a ready-made human-readable sentence in the request's language.

Error When
404 No active card with this barcode in your company
403 stamps is more than the template's stamps-per-visit limit. The body includes allowed
429 The card's stamp cooldown is active. Wait Retry-After seconds (also in the body as retry_after)
#POST /stamping/redeem

✎ Changes data.

Redeem one or more of a card's pending rewards, e.g. when the customer is handed their free coffee.

Body field Type
barcode_value string Required
count integer 1–20, default 1. How many pending rewards to redeem
notes string Free text, visible to the merchant
idempotency_key string Send one. A retried redeem without a key uses up a second reward, and that can't be undone over the API
{"barcode_value": "SC-1540CD2A802A", "idempotency_key": "order-10482-redeem"}

Response 200: the card object after redeeming, with pending_rewards lowered.

Error When
404 No card with this barcode in your company
409 Fewer pending rewards than count. The body includes pending and requested
{"detail": "No reward is ready to redeem on this card.", "pending": 0, "requested": 1}

Whether a reward can be redeemed depends on pending_rewards, not on the card's status. A card can be active with rewards waiting.

#POST /stamping/set-count

✎ Changes data.

Set a card's current_stamps to an exact number, e.g. to carry over a paper punch card when the customer joins, or to fix a missed or doubled stamp. Because the value is absolute, a retry can't over-apply it.

Body field Type
barcode_value string Required
current_stamps integer Required. 0–1000
notes string Free text, visible to the merchant
idempotency_key string Recommended. Don't reuse a key already used for a stamp on this card; see Idempotency
  • Raising the count counts as real stamps, just like /stamping/stamp. Setting 10 on a repeating 8-stamp card at 0 earns one reward and leaves current_stamps at 2. On a one-time card it earns the reward and completes the card. The cooldown and stamps-per-visit limits don't apply.
  • Lowering the count is a correction. It doesn't earn rewards, doesn't remove rewards already earned, and doesn't lower total_stamps_earned.
  • Both fire a stamp.added event with source: "adjust". When the count was lowered, stamps_added is negative.

Response 200: same shape as /stamping/stamp.

Error When
404 No card with this barcode in your company
409 The card isn't active. The body includes its status
#POST /stamping/lookup

Get a card by barcode.

{"barcode_value": "SC-1540CD2A802A"}

Response 200: the card object. Returns 404 if there's no such card in your company.

#GET /integrations/cards/search

Same as /stamping/lookup, but a miss returns 200 [] instead of 404. Use it where "no such card" is a normal answer rather than an error, such as on platforms that stop a workflow on any 4xx.

Query param
barcode_value Required

Response 200: a list containing the card object, or [].

#GET /stamp-cards/{card_id}/rewards

A card's rewards, newest first, at most 50. card_id is the card's id, not its barcode.

Query param
status Optional: pending, redeemed or voided
[
  {
    "id": "e4a6c8e0-2b4d-4f6a-8c0e-2a4c6e8a0c3b",
    "completion_number": 1,
    "status": "redeemed",
    "earned_at": "2026-08-11T13:27:59.253Z",
    "redeemed_at": "2026-08-12T09:02:11.910Z",
    "voided_at": null,
    "notes": ""
  }
]

completion_number is 1 for the card's first reward, 2 for the second, and so on. Returns 404 if there's no such card in your company.


#Customers

#GET /integrations/customers/search

Find customers and the cards they hold. Results are newest customers first, paginated.

Query param
q Optional. Matches an exact email, an exact phone number, or part of the display name (case-insensitive). Omit it to list every customer
template_slug Only customers holding a card from this template, and only those cards
store_id Only customers holding a card from this store's templates or from company-wide templates
page Default 1
page_size Default 20, max 100
{
  "items": [
    {
      "customer_id": "5e7f9a1b-3c5d-4e7f-9a1b-3c5d7e9f1a2b",
      "email": "ana@example.com",
      "phone": "+15551234567",
      "display_name": "Ana",
      "cards": [
        {
          "card_id": "2b4d6f8a-0c2e-4a6c-8e0a-2c4e6a8c0e4d",
          "barcode_value": "SC-1540CD2A802A",
          "template_id": "c7d9e1f3-2a4b-4c6d-8e0f-1a3b5c7d9e2f",
          "template_slug": "coffee-card",
          "template_name": "Coffee Card",
          "current_stamps": 3,
          "total_stamps_required": 8,
          "status": "active",
          "completions": 1,
          "pending_rewards": 1
        }
      ]
    }
  ],
  "total": 1,
  "page": 1,
  "page_size": 20
}

email and phone are empty strings when the customer didn't provide them.


#Templates and stores

#GET /integrations/templates

The company's templates (stamp card designs), active ones first, then by name. Useful for filling a dropdown.

Query param
active_only true to leave out inactive templates. Default false
limit Optional, max 200
[
  {
    "id": "c7d9e1f3-2a4b-4c6d-8e0f-1a3b5c7d9e2f",
    "slug": "coffee-card",
    "name": "Coffee Card",
    "total_stamps_required": 8,
    "reward_description": "Free coffee",
    "is_active": true,
    "store_id": "8a4e6c2b-1d3f-4e5a-b7c9-0f2e4d6a8b13",
    "store_name": "Downtown"
  }
]

store_id and store_name are null for a company-wide template.

#GET /stores/

The company's stores, oldest first.

Query param
limit Optional, max 200
[
  {
    "id": "8a4e6c2b-1d3f-4e5a-b7c9-0f2e4d6a8b13",
    "name": "Downtown",
    "slug": "downtown",
    "address": "123 Main St\nSpringfield",
    "latitude": 40.7128,
    "longitude": -74.006,
    "email": "",
    "phone": "",
    "website": "",
    "timezone": "America/New_York",
    "enrollment_notify_mode": "off",
    "referrals_enabled": true,
    "is_active": true,
    "logo": null,
    "company_id": "3f1c2a9e-5b7d-4c11-9a2e-6d0b8f4e7a21",
    "company_slug": "luna-coffee",
    "allow_cross_store_stamping": false,
    "created_at": "2026-07-28T13:47:04.914Z"
  }
]
#GET /cards/{slug}/enrollment-url

The link a customer opens to add a template's card to their wallet. slug is the template's slug. qr_data is the string to encode in a QR code; today it's the same as url.

{
  "url": "https://app.getponch.com/enroll/luna-coffee/downtown/",
  "qr_data": "https://app.getponch.com/enroll/luna-coffee/downtown/"
}
Error When
404 No template with this slug, or it's a company-wide template. Join links belong to a store, so only store templates have one

#Webhooks and events

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)