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 |
GET /integrations/meWho 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.
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 |
/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.total_stamps_earned.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/lookupGet 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/searchSame 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}/rewardsA 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.
GET /integrations/customers/searchFind 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.
GET /integrations/templatesThe 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-urlThe 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 |
POST, GET, PATCH
and DELETE /integrations/webhooks, plus rotating the secret, the delivery
log and replaying a delivery.GET /integrations/events.commit 5e140ef5493db7e98830f4b7ef4d620de5419518 Author: Peter Sanchez <peter@netlandish.com> Date: 2026-10-20T06:45:49-06:00 add proper title and descriptions to each page