Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/generated-types-pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@passmint/node': minor
---

Response types are now derived from the API's OpenAPI spec. Every 0.4.0 type name is still exported. A few types, such as `TemplateDesign` and `PassImages`, are still hand-written.

New:

- `passes.createDownloadLink()` and `passes.redemptions()`.
- `startingAfter` on list methods, and `listAll()` auto-pagination on `passes`, `templates`, `webhooks` and `events`, plus `passes.listAllEvents()` and `webhooks.listAllDeliveries()`.
- `templates.list()` takes `includeArchived`, `limit` and `startingAfter`.
- Types for `issuerName`, `locations` and `relevantDate` on template designs, `redemption_policy`, `require_download_link`, the `delivery` and `warnings` fields on pass updates, `warnings` on pass voids and on templates.
- The `pass.update_failed` and `pass.redeemed` webhook events.
- `PassEventType` is a new export.

Runtime change: `PASSMINT_EVENT_TYPES` now has 9 entries (adds `pass.update_failed` and `pass.redeemed`). Code that subscribes webhooks with `events: [...PASSMINT_EVENT_TYPES]` will start receiving those events.

Type-level changes:

- Response types gained fields the API was already returning, for example `Pass.redeemed_count` and `Account.plan`. Reading and casting are unaffected, but object literals you type as `Pass`, `Template` or `Account` by hand need the new fields.
- Event type unions gained `pass.update_failed` and `pass.redeemed`, and `PassEvent['type']` gained `update_failed`, `redeemed`, `download_link_created` and `update_not_delivered`. Exhaustive switches and `Record<PassmintEventType, …>` maps will flag the new members.
- `Pass.field_values` and `Account.organization_slug` keep their 0.4.0 types for compatibility; see their docs for what the API can actually return.
- Exported response types are now type aliases rather than interfaces. `extends` and `implements` still work; declaration merging into them no longer does.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
node-version: '22'
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm generate:check
- run: pnpm lint
- run: pnpm type-check
- run: pnpm build
Expand Down
44 changes: 36 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,34 @@ API keys starting with `pmk_test_` run against test mode, `pmk_live_` run agains
| --- | --- |
| `create(params, options?)` | Issue a new pass against a template. |
| `retrieve(id)` | Fetch a pass by id. |
| `list(params?)` | List passes, filter by template or holder email. |
| `update(id, params, options?)` | Update field values or metadata on a pass. |
| `void(id)` | Void a pass. |
| `list(params?)` | List passes, filter by template or holder email. Takes `limit` and `startingAfter`. |
| `listAll(params?)` | Async iterator over every matching pass (see Pagination). |
| `update(id, params, options?)` | Update field values or metadata on a pass. Resolves with the pass plus `delivery` (per-wallet push counts) and `warnings`. |
| `void(id)` | Void a pass. Resolves with the pass plus `warnings`. |
| `uploadImage(id, slot, data)` | Give this pass its own `strip`, `thumbnail` or `background` image. Returns the updated pass. |
| `deleteImage(id, slot)` | Remove the pass's own image for a slot. Returns the updated pass. |
| `events(id)` | List raw lifecycle events for a pass (internal vocabulary, e.g. `installed`). Use `passmint.events` for the canonical `pass.*` stream. |
| `events(id, params?)` | List raw lifecycle events for a pass (internal vocabulary, e.g. `installed`). Use `passmint.events` for the canonical `pass.*` stream. Takes `limit` and `startingAfter`; `listAllEvents(id)` iterates every page. |
| `redemptions(id, params?)` | The pass's scan log: accepted and rejected redemption attempts. |
| `createDownloadLink(id, params?)` | Create a short-lived download link for the pass (`expiresIn` in seconds). |

`create` and `update` accept an optional `idempotencyKey` in `options`. If omitted, the SDK generates one for you so retries are safe by default.

`create` also accepts `platforms` (`['apple', 'google']`) and `certificateSetId` overrides. The created pass includes `warnings` for platforms that could not be delivered.

`create` and `update` accept `imageVariant` to render the pass with one of its template's image variants (see below). On `update`, `null` clears it back to the base images.

#### Download links and redemptions

```ts
const link = await passmint.passes.createDownloadLink(pass.id, { expiresIn: 3600 })
console.log(link.url, link.expires_at)

const { data: scans } = await passmint.passes.redemptions(pass.id, { limit: 20 })
for (const scan of scans) console.log(scan.decision, scan.reason, scan.scanned_at)
```

Templates take a `redemptionPolicy` (`{ mode: 'single_use' | 'reusable' | 'count', maxUses? }`) and `requireDownloadLink` on `create` and `update`. Template responses include `warnings` when the saved design breaks a recommended rule, and template designs accept `issuerName`, `locations` and `relevantDate`.

#### Per-pass images

A pass can have its own `strip`, `thumbnail` or `background` image, such as a member photo:
Expand All @@ -163,7 +178,8 @@ Images are PNG, JPEG or WebP, up to 8 MB. Both methods re-render the pass and pu
| --- | --- |
| `create(params, options?)` | Create a template. |
| `retrieve(id)` | Fetch a template by id. |
| `list()` | List all templates. |
| `list(params?)` | List templates. Archived ones are left out unless `includeArchived: true`. Takes `limit` and `startingAfter`. |
| `listAll(params?)` | Async iterator over every matching template. |
| `update(id, params)` | Update name, design, platforms, credentials, or archive state. |
| `archive(id)` | Archive a template. |
| `uploadImage(id, slot, data, options?)` | Upload a PNG/JPEG (`Uint8Array`, `ArrayBuffer` or `Buffer`) to an image slot, optionally as a named `variant`. |
Expand Down Expand Up @@ -210,7 +226,19 @@ const { data, has_more } = await passmint.events.list({
const next = await passmint.events.list({ startingAfter: data.at(-1)!.id })
```

Canonical event types: `pass.issued`, `pass.add_intent`, `pass.added_to_wallet`, `pass.update_pushed`, `pass.update_delivered`, `pass.removed`, `pass.voided` (exported as `PASSMINT_EVENT_TYPES`).
Canonical event types: `pass.issued`, `pass.add_intent`, `pass.added_to_wallet`, `pass.update_pushed`, `pass.update_delivered`, `pass.update_failed`, `pass.removed`, `pass.voided`, `pass.redeemed` (exported as `PASSMINT_EVENT_TYPES`).

### Pagination

List endpoints return `{ data, has_more }` and take `startingAfter` (the last item's id). To walk every page, use `listAll`, which fetches pages as you iterate and stops when `has_more` is false:

```ts
for await (const pass of passmint.passes.listAll({ templateId: 'tmpl_123' })) {
console.log(pass.id)
}
```

`passes.redemptions` takes `limit` only (no `startingAfter`). `listAll` exists on `passes`, `templates`, `webhooks` and `events`. For your own list calls, `autoPaginate((startingAfter) => fetchPage(startingAfter))` is exported too.

### Metrics — `passmint.metrics`

Expand All @@ -233,11 +261,11 @@ Manage webhook endpoints:
| Method | Description |
| --- | --- |
| `create(params)` | Create an endpoint. Returns the signing `secret` **once** — store it. |
| `retrieve(id)` / `list()` | Fetch endpoints. |
| `retrieve(id)` / `list(params?)` | Fetch endpoints. `list` takes `limit` and `startingAfter`; `listAll()` iterates every page. |
| `update(id, params)` | Change url, subscribed events, description, or enabled state. |
| `delete(id)` | Delete an endpoint. |
| `backfill(id, { since, until? })` | Re-emit historical events to one endpoint. |
| `deliveries(id)` | Last 100 delivery attempts, newest first. |
| `deliveries(id, params?)` | Delivery attempts, newest first (last 100 by default). Takes `limit` and `startingAfter`; `listAllDeliveries(id)` iterates every page. |
| `replayDelivery(id, deliveryId)` | Re-queue a delivery (including dead ones). |

```ts
Expand Down
10 changes: 9 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@
"useIgnoreFile": true
},
"files": {
"ignore": ["dist", "node_modules", ".turbo", "coverage", "ecosystem-tests"]
"ignore": [
"dist",
"node_modules",
".turbo",
"coverage",
"ecosystem-tests",
"src/generated",
"openapi.json"
]
},
"linter": {
"enabled": true,
Expand Down
12 changes: 11 additions & 1 deletion ecosystem-tests/bun/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,17 @@ assert.equal(typeof Passmint, 'function')
assert.equal(typeof PassmintError, 'function')
assert.equal(typeof PassmintAuthError, 'function')
assert.equal(typeof PassmintRateLimitError, 'function')
assert.equal(PASSMINT_EVENT_TYPES.length, 7)
assert.deepEqual([...PASSMINT_EVENT_TYPES], [
'pass.issued',
'pass.add_intent',
'pass.added_to_wallet',
'pass.update_pushed',
'pass.update_delivered',
'pass.update_failed',
'pass.removed',
'pass.voided',
'pass.redeemed',
])
assert.equal(detectMode(apiKey), 'test')

const passmint = new Passmint({ apiKey, baseUrl })
Expand Down
12 changes: 11 additions & 1 deletion ecosystem-tests/deno/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,17 @@ assert.equal(typeof Passmint, 'function')
assert.equal(typeof PassmintError, 'function')
assert.equal(typeof PassmintAuthError, 'function')
assert.equal(typeof PassmintRateLimitError, 'function')
assert.equal(PASSMINT_EVENT_TYPES.length, 7)
assert.deepEqual([...PASSMINT_EVENT_TYPES], [
'pass.issued',
'pass.add_intent',
'pass.added_to_wallet',
'pass.update_pushed',
'pass.update_delivered',
'pass.update_failed',
'pass.removed',
'pass.voided',
'pass.redeemed',
])
assert.equal(detectMode(apiKey), 'test')

const passmint = new Passmint({ apiKey, baseUrl })
Expand Down
2 changes: 2 additions & 0 deletions ecosystem-tests/mock-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ const CANONICAL_EVENT_TYPES = [
'pass.added_to_wallet',
'pass.update_pushed',
'pass.update_delivered',
'pass.update_failed',
'pass.removed',
'pass.voided',
'pass.redeemed',
]

function errorBody(type, message, extra = {}) {
Expand Down
12 changes: 11 additions & 1 deletion ecosystem-tests/node-js/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,17 @@ assert.equal(typeof Passmint, 'function')
assert.equal(typeof PassmintError, 'function')
assert.equal(typeof PassmintAuthError, 'function')
assert.equal(typeof PassmintRateLimitError, 'function')
assert.equal(PASSMINT_EVENT_TYPES.length, 7)
assert.deepEqual([...PASSMINT_EVENT_TYPES], [
'pass.issued',
'pass.add_intent',
'pass.added_to_wallet',
'pass.update_pushed',
'pass.update_delivered',
'pass.update_failed',
'pass.removed',
'pass.voided',
'pass.redeemed',
])
assert.equal(detectMode(apiKey), 'test')

const passmint = new Passmint({ apiKey, baseUrl })
Expand Down
Loading
Loading