Authentication

Every API request must be authenticated. We use two custom HTTP headers for authentication. The first is an API key, the second is an account ID - both of these can be found in your account dashboard under pass template "send" page for a given pass in the Pass Templates tab. The API key should be sent in the X-API-KEY header and the Account ID should be sent in the X-ACCOUNT-ID header.

Note:
You should only send traffic over HTTPs and do your best to keep your authentication credentials private.

Parameter Reference

In this documentation, certain identifiers are represented using placeholders to ensure clarity and consistency across examples. These placeholders indicate where developers should substitute their own values while also highlighting the expected format.

Placeholder Description Format Example
<YOUR_API_KEY> Represents your API key for authentication <32-char hex> 91dbb1e7af9c80aba6ed3f95a1545b4e
<YOUR_ACCOUNT_ID> Represents a unique account identifier aid_0x<hex> aid_0xa01
<YOUR_TEMPLATE_ID> Represents a pass template identifier ptk_0x<hex> ptk_0x14
<SERIAL_NUMBER> Represents a unique pass identifier (also known as passId) <18-char hex> 838b8cf60dac2bfad9;

Pass Templates

Pass Templates are created from your account and customized with the pass template editor. They represent a template to create a pass from. Pass Templates have a pass template identifier that is present in the Get Passes tab for a given pass template, it takes the form of <YOUR_TEMPLATE_ID> and is required to be passed in when creating a pass.

Furthermore, pass templates allow you to define the JSON keys you will use to populate your pass upon creation.

List pass templates

Retrieves a list of all pass templates for your account, including metadata about each template such as the number of issued and installed passes.

This endpoint requires no parameters and returns an array of pass template objects.

Response Fields:
id - The pass template identifier (e.g., <YOUR_TEMPLATE_ID>)
name - Template name
platform - Platform (apple or google)
style - Pass style
issuedPassCount - Total passes issued
installedPassCount - Total passes installed
createdAt - Creation timestamp
updatedAt - Last update timestamp
curl -X GET 'https://api.passninja.com/v1/pass_templates' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

You will get a response with an array of pass template objects. Example response:

{ "pass_templates": [ { "id": "<YOUR_TEMPLATE_ID>", "name": "Loyalty Card", "platform": "apple", "style": "loyalty", "issuedPassCount": 150, "installedPassCount": 89, "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-03-20T14:45:00Z" } ] }

Get a pass template

Retrieves details about a specific pass template using its template ID.

Below is the required parameter for retrieving a pass template:

  • id* string

    The pass template identifier, present in the Get Passes tab for a given pass template. It looks like this: <YOUR_TEMPLATE_ID>

Response Fields:
id - The pass template identifier
name - Template name
platform - Platform (apple or google)
style - Pass style
issuedPassCount - Total passes issued
installedPassCount - Total passes installed
curl -X GET 'https://api.passninja.com/v1/pass_templates/<YOUR_TEMPLATE_ID>' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

You will get a response with a pass template object. Example response:

{ "id": "<YOUR_TEMPLATE_ID>", "name": "Loyalty Card", "platform": "apple", "style": "loyalty", "issuedPassCount": 150, "installedPassCount": 89 }

Get pass template fields

Retrieves the full field schema for a pass template — every API mapped field with its visibility and requirement flags. Use this to discover which fields a create or update call may set; fields marked required must be supplied.

Below is the required parameter for retrieving a template's fields:

  • id* string

    The pass template identifier, present in the Get Passes tab for a given pass template. It looks like this: <YOUR_TEMPLATE_ID>

Response Fields:
id - The pass template identifier
platform - Platform (apple or google)
fields - Array of field objects, each with:
    ◦ api_field_name - The field key to set on create/update
    ◦ visible - Boolean; whether the field is shown on the pass
    ◦ required - Boolean; whether a create/update call must supply it
curl -X GET 'https://api.passninja.com/v1/pass_templates/fields/<YOUR_TEMPLATE_ID>' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

You will get a response listing every field on the template. Example response (truncated):

{ "id": "<YOUR_TEMPLATE_ID>", "platform": "apple", "fields": [ { "api_field_name": "account.name", "visible": true, "required": true }, { "api_field_name": "nfc.message", "visible": true, "required": false }, { "api_field_name": "barcode.data", "visible": false, "required": false } ] }

Get reader config

Returns the reader-agnostic identity values and EC decryption keys a physical NFC reader needs to read this template's Apple (VAS) and/or Google (Smart Tap) passes. It carries no reader-specific concepts (key slots, file names, config-file fields) — the device tooling that consumes it decides those. Only decryption keys are returned; the platform signing certificate / key are never exposed.

Below is the required parameter:

  • id* string

    The pass template identifier — looks like <YOUR_TEMPLATE_ID>. Must belong to the calling account; otherwise 404 is returned.

curl -X GET 'https://api.passninja.com/v1/pass_templates/<YOUR_TEMPLATE_ID>/reader_config' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

You will get a 200 response. The apple object carries the Apple Pass Type ID (vas_merchant_id) plus the VAS decryption key; the google object carries the Smart Tap collector id, key version, and decryption key. The side that the template's platform does not cover — or that has no usable key — is null. Example for an Apple template:

{ "id": "ptk_0x216", "platform": "apple", "apple": { "vas_merchant_id": "pass.com.passninja.loyalty", "vas_private_key_pem": "-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----\n" }, "google": null }

A Google template returns the google object populated and apple as null:

{ "id": "ptk_0x217", "platform": "google", "apple": null, "google": { "smart_tap_collector_id": "77501435", "smart_tap_key_version": "1", "smart_tap_private_key_pem": "-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----\n" } }

Create a pass template Premium

Programmatically provision a new pass template under your account. The template is seeded with its pass type's default fields, so you can issue passes against it right away; refine its visible fields, art, and styling through the pass template editor in the dashboard or the Update endpoint below.

Requires the enterprise-api-service entitlement, honored whether it is granted at the account level (enterprise plan) or on the pass template (basic / premium plan); without it the call returns 403.

Below is the list of acceptable params for creating a pass template. * denotes a required parameter:

  • name* string

    Human label for the template, ≤ 120 characters.

  • platform* string

    One of apple or google. A pass template targets a single wallet — to support both Apple and Google, create one template per platform.

  • style* string

    Pass style identifier (e.g. access_control, coupon, ticket, loyalty). Must be valid for the chosen platform. See Platform Parameters for the full list.

  • install_constraints object

    Limit where each issued pass can install. Any of device, browser, ip (booleans); omitted keys are left unchanged. Requires the install-constraints feature, otherwise 403.

  • disable_sharing object

    Stop passes from being shared between devices/holders, per platform: apple and/or google (booleans; true disables sharing). Requires the disable-sharing feature, otherwise 403.

  • top_up object

    Auto-refill the prepaid balance: auto_recharge (boolean), balance_trigger ($10–$2000), top_up_target ($20–$4000, must exceed the trigger). Available to per-template subscribers only — accounts whose plan covers usage at the account level get 403. Editing never charges the card; changes apply on the next monthly top-up.

  • reader string

    Optionally pair the new template with a reader at creation time (the reader id). Valid once the template has exactly one application defined; the server binds that application to the reader with the same validations as the readers API.

curl -X POST 'https://api.passninja.com/v1/pass_templates' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>' \ -H 'Content-Type: application/json' \ -d '{ "name": "Loyalty Card", "platform": "apple", "style": "loyalty", "install_constraints": { "device": true }, "disable_sharing": { "apple": true } }'

You will get a 201 response with the created pass template object — note the id, which is the ptk_0x... identifier you will use for every subsequent pass operation:

{ "id": "ptk_0x216", "name": "Loyalty Card", "platform": "apple", "style": "loyalty", "issuedPassCount": 0, "installedPassCount": 0, "createdAt": "2026-05-12T18:00:00Z", "updatedAt": "2026-05-12T18:00:00Z", "install_constraints": { "device": true, "browser": false, "ip": false }, "disable_sharing": { "apple": true, "google": false }, "top_up": { "auto_recharge": false, "balance_trigger": null, "top_up_target": null } }

Update a pass template Premium

Update an existing template's name, its scalar field settings, and its install-constraint, disable-sharing, and auto top-up configuration. Use PATCH for a partial update; PUT shares the same handler. Changes made through the API publish immediately.

Requires the enterprise-api-service entitlement, honored whether it is granted at the account level (enterprise plan) or on the pass template (basic / premium plan); without it the call returns 403. Provide at least one of the params below.

  • id* string

    The pass template identifier in the path — looks like <YOUR_TEMPLATE_ID>. Must belong to the calling account; otherwise 404 is returned.

  • name string

    New human label for the template, ≤ 120 characters.

  • fields object

    An object keyed by api field name; each value may set any of default_value, visible, required, and api_field_name (omitted keys are left unchanged). The full set of api field names for each platform / style is documented under Platform Parameters. Setting api_field_name remaps a field's external key — e.g. address the field by its current key primary.value and set api_field_name to guest.name to issue passes against the new name. Keys that match neither an existing field nor a documented api field name are rejected with 400 before any write.

  • install_constraints object

    Limit where each issued pass can install. Any of device, browser, ip (booleans); omitted keys are left unchanged. Requires the install-constraints feature, otherwise 403.

  • disable_sharing object

    Stop passes from being shared between devices/holders, per platform: apple and/or google (booleans; true disables sharing). Requires the disable-sharing feature, otherwise 403.

  • top_up object

    Auto-refill the prepaid balance: auto_recharge (boolean), balance_trigger ($10–$2000), top_up_target ($20–$4000, must exceed the trigger). Available to per-template subscribers only — accounts whose plan covers usage at the account level get 403. Editing never charges the card; changes apply on the next monthly top-up.

  • reader string

    Pair this template with a reader from the template side: the reader id (e.g. 42). Valid once the template has exactly one application defined — the server binds that application to the reader (idempotent, same validations as the readers API). The template must have no issued passes at pairing time. Returns 422 if the template has no application or more than one.

Example — edit a field's default value and remap another field's api field name:

curl -X PATCH 'https://api.passninja.com/v1/pass_templates/<YOUR_TEMPLATE_ID>' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>' \ -H 'Content-Type: application/json' \ -d '{ "fields": { "primary.value": { "api_field_name": "guest.name" }, "background.color": { "default_value": "rgb(0, 0, 255)" } } }'

Returns 200 with the updated template, its configuration groups, and the resulting api-mapped fields:

{ "id": "ptk_0x216", "name": "Loyalty Card", "platform": "apple", "style": "loyalty", "issuedPassCount": 0, "installedPassCount": 0, "createdAt": "2026-05-12T18:00:00Z", "updatedAt": "2026-05-12T18:05:00Z", "install_constraints": { "device": true, "browser": false, "ip": true }, "disable_sharing": { "apple": true, "google": false }, "top_up": { "auto_recharge": false, "balance_trigger": null, "top_up_target": null }, "fields": [ { "api_field_name": "guest.name", "default_value": "", "visible": true, "required": false }, { "api_field_name": "background.color", "default_value": "rgb(0, 0, 255)", "visible": true, "required": false } ] }

Delete a pass template Premium

Permanently removes a pass template from your account. This is destructive and irreversible — issued passes attached to the template are deleted along with it, installed wallet copies stop receiving updates, and the ptk_0x... identifier becomes invalid for all future API calls.

Below is the required parameter for deleting a pass template:

  • id* string

    The pass template identifier — looks like <YOUR_TEMPLATE_ID>. Must belong to the calling account; otherwise 404 is returned.

curl -X DELETE 'https://api.passninja.com/v1/pass_templates/<YOUR_TEMPLATE_ID>' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

Returns 204 No Content on success.

Create a new pass

Creating a pass allows you to create a new pass for a given pass template with given values. In addition to issuance, for an additional $0.05 you can send the pass via text message. Returns pass id (also known as a serial number), and when applicable message_status.

This creates an "Active pass" which incurs a monthly cost. Please see pricing for more info.

Note:
For self-serve customers, if you send text messages you will be rate limited to a maximum of 10 requests per second. For premium customers, this rate limit is 100 requests per second when sending passes via text message.

Below is a list of acceptable params for creating a pass. * denotes a required parameter:

  • passTemplate* string

    This is the pass template identifier, it is present in the Get Passes tab for a given pass template. It looks like this: <YOUR_TEMPLATE_ID>

  • pass* object

    The keys you have set as Visible on the Edit Pass Template page. Use the API mapping field names, not the Apple or Google property names.

  • recipient (optional) object

    The name, message and phone number or email of the person you wish to issue this pass. The parameters here are not used to create passes, only to send them. Acceptable params are as follows:

    • name* string

      The name of the person receiving the pass.

    • message(optional) string

      The message sent with the pass. Use @name, @account or @url to customize the message. For example: "Hi @name, @account sent you this pass @url"

    • sms (optional) string

      Internationally formatted phone number to send message to, with a + in front. For example: +15613437899

    • email (optional) string

      RFC compliant email address to send message to. For example: support@passninja.com

curl -X POST 'https://api.passninja.com/v1/passes' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>' \ -H 'Content-Type: application/json' \ -d '{ "passTemplate": "<YOUR_TEMPLATE_ID>", "pass": { "member.level": "silver", "discount": "50%", "member.name": "John" } }'

You will get a response with the created pass object. Example response:

{ "id": "<SERIAL_NUMBER>", "passTemplate": "<YOUR_TEMPLATE_ID>", "serialNumber": "<SERIAL_NUMBER>", "urls": { "landing": "https://i.installpass.es/p/<SERIAL_NUMBER>" } }

List passes

Allows you to retrieve a list of passes using its pass template identifier. This is useful for understanding the issued and installed dates as well as the current status of your pass inventory. Supports pagination and search.

Below is a list of params:

  • passTemplate* string

    This is the pass template identifier, it is present in the Get Passes tab for a given pass template. It looks like this: <YOUR_TEMPLATE_ID>

  • page integer (optional)

    Page number for pagination. When provided, the response includes a meta object with pagination details. Without this parameter, all passes are returned.

  • perPage integer (optional)

    Number of passes per page. Defaults to 10. Only applies when page is provided. The legacy per_page alias is still accepted for backward compatibility but will be removed in a future release.

  • search string (optional)

    Search passes by serial number. Performs a case-insensitive partial match.

Response Fields:
id - The pass serial number
passTemplate - The pass template identifier
serialNumber - Same as id
urls.landing - Download URL for the pass
issuedDate - When pass was created
installedDate - When pass was installed (null if not installed)
status - Pass status (Active, Removed, Expired, or N/A)
recipient - Delivery info (contains sms and/or email if the pass was sent to a recipient, null otherwise)
meta - Pagination metadata (only present when page param is used): currentPage, totalPages, totalCount, perPage.
curl -X GET 'https://api.passninja.com/v1/passes/<YOUR_TEMPLATE_ID>?page=1&perPage=10&search=abc123' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

You will get a response with an array of pass objects. Example response:

{ "passes": [ { "id": "<SERIAL_NUMBER>", "passTemplate": "<YOUR_TEMPLATE_ID>", "serialNumber": "<SERIAL_NUMBER>", "urls": { "landing": "https://i.installpass.es/p/<SERIAL_NUMBER>" }, "issuedDate": "2024-04-23T10:15:00Z", "installedDate": "2024-04-23T14:30:00Z", "status": "Active", "recipient": { "sms": "+15551234567", "email": "user@example.com" } } ], "meta": { "currentPage": 1, "totalPages": 5, "totalCount": 42, "perPage": 10 } }

Get a pass

Allows you to retrieve a pass using its pass template identifier and serial number. The response uses the API mapping field names that have been defined and set to Visible in your pass template, matching the field names used by the Create, Patch, and Update methods. Note that pass status and install date information is only available in the List passes response, not in the individual Get a pass response.

Below is a list of required params for retrieving a pass:

  • passTemplate* string

    This is the pass template identifier, it is present in the Get Passes tab for a given pass template. It looks like this: <YOUR_TEMPLATE_ID>

  • passId* string

    PassId also known as a serial number. Takes on a namespaced format which looks like <SERIAL_NUMBER>

Response Fields:
id - The pass serial number
passTemplate - The pass template identifier
serialNumber - Same as id
urls.landing - Download URL for the pass
pass - Object containing all pass field values
curl -X GET 'https://api.passninja.com/v1/passes/<YOUR_TEMPLATE_ID>/<SERIAL_NUMBER>' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

You will get a response with the pass object including all field values. Example response:

{ "id": "<SERIAL_NUMBER>", "passTemplate": "<YOUR_TEMPLATE_ID>", "serialNumber": "<SERIAL_NUMBER>", "urls": { "landing": "https://i.installpass.es/p/<SERIAL_NUMBER>" }, "pass": { "member.level": "gold", "discount": "100%", "member.name": "John" } }
Note:
To retrieve the pass object with the raw Apple or Google property names and structure instead of the API mapping field names, append /raw to the request URL:
GET /v1/passes/<YOUR_TEMPLATE_ID>/<SERIAL_NUMBER>/raw

Decrypt a pass

Allows you to decrypt a pass payload using its pass template identifier and encrypted payload. This is useful when you're working with an APDU-based reader that doesn't have an onboard hardware secure module (HSM) for decryption. Contact us via the website chat if you have questions about using this.

The endpoint supports both wallet platforms. For Apple VAS (the default) the captured hex payload decrypts with the template's long-term key alone. For Google Smart Tap the ciphertext is session-bound: the reader must also send the tap's session context — both nonces, both ephemeral public keys, the terminal ephemeral private key, and the session signature (obtained from the Smart Tap pre-sign endpoint). The collector id and key version are resolved server-side from the template and are never supplied by the client.

Below is a list of params for decrypting a pass:

  • passTemplate* string

    This is the pass template identifier, it is present in the Get Passes tab for a given pass template. It looks like this: <YOUR_TEMPLATE_ID>

  • platform string

    Either apple (default) or google. Selects the decryption scheme; when omitted it follows the template's platform.

  • payload* string

    Payload is a hex encoded APDUs with no spaces. It looks like this (abbreviated for presentation): 55166a97002...1ea070f0d4fe88887

  • session object

    Required when platform is google. The Smart Tap session context, all hex-encoded: terminalNonce, mobileNonce, terminalEphemeralPublicKey, terminalEphemeralPrivateKey, mobileEphemeralPublicKey, and signature. Not used for Apple VAS.

Apple VAS — decrypt with just the payload:

curl -X POST 'https://api.passninja.com/v1/passes/<YOUR_TEMPLATE_ID>/decrypt' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>' \ -H 'Content-Type: application/json' \ -d '{ "platform": "apple", "payload": "55166a9700250a8c51382dd16822b0c763136090b91099c16385f2961b7d9392d31b386cae133dca1b2faf10e93a1f8f26343ef56c4b35d5bf6cb8cd9ff45177e1ea070f0d4fe88887" }'

Google Smart Tap — include the pre-signed session context:

curl -X POST 'https://api.passninja.com/v1/passes/<YOUR_TEMPLATE_ID>/decrypt' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>' \ -H 'Content-Type: application/json' \ -d '{ "platform": "google", "payload": "<HEX_IV_CIPHERTEXT_HMAC>", "session": { "terminalNonce": "<HEX>", "mobileNonce": "<HEX>", "terminalEphemeralPublicKey": "<HEX>", "terminalEphemeralPrivateKey": "<HEX>", "mobileEphemeralPublicKey": "<HEX>", "signature": "<HEX>" } }'

You will get a 200 response with the decrypted value. Apple additionally returns timeOfScan from inside the encrypted envelope:

{ "encrypted": "55166a97002...1ea070f0d4fe88887", "decrypted": "fee4a257185906b92a", "timeOfScan": "2026-07-24T18:00:00Z" }

Patch a pass

Allows you to partially update a pass using its pass template identifier, serial number, and new information. Unlike PUT, fields that are not provided will retain their existing values. This update will occur as soon as possible, but may not be instant - it will depend on the service of the users device.

Below is a list of required params for patching a pass:

  • passTemplate* string

    This is the pass template identifier, it is present in the Get Passes tab for a given pass template. It looks like this: <YOUR_TEMPLATE_ID>

  • passId* string

    PassId also known as a serial number. Takes on a namespaced format which looks like <SERIAL_NUMBER>

  • pass* object

    The keys you have set as visible for this template on the Edit Pass Template page. These are the API mapping field names, which do not match the property names returned by the Get a pass response. Only include the fields you want to update.

curl -X PATCH 'https://api.passninja.com/v1/passes/<YOUR_TEMPLATE_ID>/<SERIAL_NUMBER>' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>' \ -H 'Content-Type: application/json' \ -d '{ "pass": { "member.name": "Ted" } }'

You will get a response with the updated pass object including all field values. Example response:

{ "id": "<SERIAL_NUMBER>", "passTemplate": "<YOUR_TEMPLATE_ID>", "serialNumber": "<SERIAL_NUMBER>", "urls": { "landing": "https://i.installpass.es/p/<SERIAL_NUMBER>" } }

Note: In the example above, only member.name was updated. All other fields retained their previous values.

Clearing a field: send null or an empty string as the value to remove the field's stored value from the pass, e.g. {"pass": {"event.start.date": null}}. The pass falls back to whatever the template currently publishes for that field (nothing, if the template sets no default). Required fields cannot be cleared.

Update a pass

Allows you to update a pass using its pass template identifier, serial number, and new information. Fields that are not provided will revert to their pass template default values. This update will occur as soon as possible, but may not be instant - it will depend on the service of the users device.

Below is a list of required params for updating a pass:

  • passTemplate* string

    This is the pass template identifier, it is present in the Get Passes tab for a given pass template. It looks like this: <YOUR_TEMPLATE_ID>

  • passId* string

    PassId also known as a serial number. Takes on a namespaced format which looks like <SERIAL_NUMBER>

  • pass* object

    The keys you have set as visible for this template on the Edit Pass Template page. These are the API mapping field names, which do not match the property names returned by the Get a pass response.

curl -X PUT 'https://api.passninja.com/v1/passes/<YOUR_TEMPLATE_ID>/<SERIAL_NUMBER>' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>' \ -H 'Content-Type: application/json' \ -d '{ "pass": { "member.level": "gold", "discount": "100%", "member.name": "John" } }'

You will get a response with the updated pass object including all field values. Example response:

{ "id": "<SERIAL_NUMBER>", "passTemplate": "<YOUR_TEMPLATE_ID>", "serialNumber": "<SERIAL_NUMBER>", "urls": { "landing": "https://i.installpass.es/p/<SERIAL_NUMBER>" } }

Delete a pass

An irreversible action to destroy a pass using its pass template identifier and serial number. Will not remove the pass from users device, but will void the pass at the earliest possible moment and prevent additional billing events immediately.

Below is a list of required params for deleting a pass:

  • passTemplate* string

    This is the pass template identifier, it is present in the Get Passes tab for a given pass template. It looks like this: <YOUR_TEMPLATE_ID>

  • passId* string

    PassId also known as a serial number. Takes on a namespaced format which looks like <SERIAL_NUMBER>

curl -X DELETE 'https://api.passninja.com/v1/passes/<YOUR_TEMPLATE_ID>/<SERIAL_NUMBER>' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

You will get a response with the deleted pass serial number. Example response:

"<SERIAL_NUMBER>"

Webhooks Premium

Webhooks let your server react to pass-lifecycle events in near real time. PassNinja delivers each event as a CloudEvents 1.0 JSON envelope over HTTPS, signed with a per-subscription bearer token. Subscriptions, including their bearer secrets, are managed through the endpoints below; the entire /v1/webhooks resource requires a premium account with an active subscription.

  • One subscription = one HTTPS endpoint + one bearer token + one event filter.
  • Subscriptions can be account-wide or scoped to a single pass template.
  • Deliveries retry with exponential backoff for ~24h; every attempt is recorded and queryable for 15 days.
  • Bearer secrets are returned once on creation and stored AES-256-GCM encrypted at rest — there is no read-back endpoint.

Event types

Event types follow the CloudEvents convention of reverse-DNS-ish dotted names. The pn.* namespace is reserved by PassNinja; do not subscribe to types outside the catalog below — subscriptions to unrecognized types are accepted but will never receive deliveries, and PassNinja may add new pn.* types in the future without prior notice.

Type Status Fires when
pn.pass.created Active A new pass is issued via POST /v1/passes.
pn.pass.installed Active A wallet (Apple or Google) registers a new device for an issued pass for the first time.
pn.pass.updated Active Pass field values are changed via PUT or PATCH /v1/passes/….
pn.pass.uninstalled Active A wallet removes the pass and de-registers its device push token.
pn.pass.deleted Active An issued pass is permanently deleted — directly via DELETE /v1/passes/…, or when the system purges it after its blackout grace period.
pn.pass.expired Active The monthly sweep purges a pass whose expirationDate has passed. Requires the purge-expired-passes entitlement on the template or account; without it, expired passes are not swept and no event fires.
pn.pass.scanned Active A reader records a scan via POST /v1/scans that resolves to an issued pass.
pn.template.* Reserved Reserved namespace for future pass-template lifecycle events (e.g. pn.template.published).

Type names are validated against ^pn\.[a-z][a-z_]*\.[a-z][a-z_]*$ on subscription create. Anything outside the pn.* namespace is rejected with HTTP 400.

Webhook payload

Each delivery is an HTTP POST to your subscription's url with Content-Type: application/json, an Authorization: Bearer <token> header, and a JSON body in CloudEvents 1.0 structured-mode.

{ "specversion": "1.0", "id": "38656a51-a892-8717-587b-f59de5163c2a", "source": "https://api.passninja.com/accounts/aid_0xa01", "type": "pn.pass.installed", "datacontenttype": "application/json", "time": "2026-05-12T18:25:43.511Z", "pnattempt": 1, "data": { "passTemplate": "ptk_0x216", "passId": "fee4a257185906b92a", "metadata": { "member.name": "John", "discount": "50%" } } }

Envelope attributes

PassNinja sets the CloudEvents 1.0 attributes listed below. Other CE-spec attributes (subject, dataschema, data_base64) are reserved by the spec and may be added in future versions; receivers should ignore unknown top-level attributes rather than failing.

Attribute Source Description
specversion CE 1.0 (required) Always the literal string "1.0".
id CE 1.0 (required) Stable identifier for the logical event. Identical across all retry attempts for the same delivery — use this as your dedupe key.
source CE 1.0 (required) URI-reference identifying the producing account, e.g. https://api.passninja.com/accounts/aid_0xa01.
type CE 1.0 (required) One of the event types listed above (e.g. pn.pass.installed).
datacontenttype CE 1.0 (optional) Always "application/json".
time CE 1.0 (optional) RFC 3339 timestamp of when the event occurred. Stable across retries.
data CE 1.0 (optional) Event-specific payload — see the data-payload table below. Byte-stable across retries; transport metadata (e.g. attempt number) is kept out of data by design.
pnattempt PassNinja extension CE extension attribute (CE 1.0 §4): integer delivery attempt counter, starting at 1. Increments on every retry up to 15. Use it for logging or replay-protection — not for dedupe (use id).

data payload (pass events)

Field Type Description
passTemplate string Pass template API key, e.g. ptk_0x216.
passId string Pass serial number returned by POST /v1/passes.
metadata object | null Snapshot of the pass's current field values keyed by template-defined paths (e.g. "member.name": "John"). null if the pass has no set fields yet.

Receiver expectations

  • Verify the Authorization header matches the bearer token returned at subscription creation. Reject anything else with HTTP 401.
  • Respond with any 2xx status to acknowledge. Anything else is treated as failure and triggers a retry.
  • Aim to ack within 10 seconds — deliveries time out after that and are retried.
  • Treat the id attribute as the dedupe key. Retries (and rare delivery duplicates) reuse the same id; pnattempt changes per attempt.
  • Ignore unknown top-level attributes — the CloudEvents spec reserves room for future ones.

Create a webhook subscription

POST /v1/webhooks

Creates a subscription and returns a freshly generated bearer token. The plaintext token is included in the response only on creation; subsequent reads omit it (it's stored AES-256-GCM encrypted at rest, with no decryption endpoint exposed). Persist it on your side at this moment.

Request body

Field Required Description
name yes Human label, ≤ 120 chars.
url yes Absolute https:// URL. http:// is rejected.
subscribedEvents yes Non-empty array of CloudEvents type strings (see Event types above).
authMethod no Defaults to "bearer_token". "mtls" is reserved (returns HTTP 501 today).
passTemplate no If set, scopes the subscription to one pass template (ptk_0x...). Omit for an account-wide subscription.
curl -X POST 'https://api.passninja.com/v1/webhooks' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>' \ -H 'Content-Type: application/json' \ -d '{ "name": "Production install handler", "url": "https://example.com/passninja/webhook", "subscribedEvents": ["pn.pass.installed", "pn.pass.uninstalled"] }'

Example 201 response — note that bearerToken appears here and nowhere else:

{ "id": "47", "name": "Production install handler", "url": "https://example.com/passninja/webhook", "authMethod": "bearer_token", "subscribedEvents": ["pn.pass.installed", "pn.pass.uninstalled"], "passTemplate": null, "active": true, "createdAt": "2026-05-12T18:00:00.000Z", "updatedAt": "2026-05-12T18:00:00.000Z", "bearerToken": "wbk_b3d6…" }

List webhook subscriptions

GET /v1/webhooks

Returns subscriptions in the calling account, newest first. Supports query params page (default 1), perPage (default 50, max 100), and passTemplate=ptk_0x... to filter by scope.

curl -X GET 'https://api.passninja.com/v1/webhooks?perPage=20' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

Get a webhook subscription

GET /v1/webhooks/<ID>

Same row shape as the list response. The bearer token is never included. Returns 404 if the id doesn't belong to the calling account.

curl -X GET 'https://api.passninja.com/v1/webhooks/47' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

Delete a webhook subscription

DELETE /v1/webhooks/<ID>

Removes the subscription and its delivery history in a single transaction. Returns 204 No Content. To pause without losing history, build subscription rotation (delete + recreate) into your tooling — there is no soft-disable endpoint today.

curl -X DELETE 'https://api.passninja.com/v1/webhooks/47' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

Inspect delivery history

GET /v1/webhooks/<ID>/results

Every delivery attempt — successful or not — writes a webhookResults row. Useful for diagnosing receiver-side outages without grepping your own logs. Same pagination params as the list endpoint. Delivery history is retained for 15 days; rows older than that are purged automatically, so export anything you need to keep for longer.

{ "webhookResults": [ { "id": "1820", "webhookId": "47", "url": "https://example.com/passninja/webhook", "responseStatus": 200, "responseBody": "{\"ok\":true}", "success": true, "attempt": 1, "createdAt": "2026-05-12T18:25:43.522Z", "updatedAt": "2026-05-12T18:25:43.522Z" } ], "page": 1, "perPage": 50, "total": 1 }

Retry policy

  • Attempts: up to 15 per delivery.
  • Backoff: exponential starting at 10 seconds, capped so the full sequence spans roughly 24 hours.
  • Success: any HTTP 2xx response from your endpoint.
  • Timeout: requests time out after 10 seconds and count as a failure.
  • Stable identity: retries reuse the same CloudEvents id and data payload; only pnattempt increments.
  • After 15: the delivery is dropped and a final failure row is written. PassNinja does not auto-disable the subscription — but a chronically failing endpoint will fill /v1/webhooks/<id>/results with failure rows (retained 15 days, then purged).

Legacy subscriptions Deprecated

Webhook subscriptions created before the CloudEvents rollout continue to receive deliveries in their original wire format — no Authorization header, no envelope, original field names — so existing receivers do not need changes:

{ "passTemplate": "ptk_0x216", "passId": "fee4a257185906b92a", "event": "install", "eventDate": "2026-05-12T18:25:43.511Z", "attempt": 1 }

No new legacy-format subscriptions can be created — the POST /v1/webhooks endpoint always issues bearer-authenticated CloudEvents subscriptions. To migrate an existing receiver: provision a new CloudEvents subscription pointed at your endpoint, switch the receiver to verify the bearer and parse the envelope, then delete the legacy row. The two formats can coexist during cutover.

Applications Premium

An application defines what happens when one of your readers scans a pass. Each application is bound to exactly one pass template and is one of three kinds: log records every resolved scan, validate accepts or rejects the tap and returns a green/red LED instruction, and forward relays the scan to your own endpoint and shuttles its answer back to the reader. Applications are the unit a reader binds to.

The log kind requires the scan-events-system entitlement; validate requires scan-events-validate and forward requires scan-events-forward. Application ids look like app_0x....

Create an application Premium

Create an application bound to one pass template. The template binding is fixed once created — make a new application to target a different template.

  • name* string

    Human label, ≤ 120 characters.

  • kind* string

    One of log, validate, forward. The validate and forward kinds require their own entitlement, otherwise 403.

  • passTemplate* string

    The template this application validates for — looks like <YOUR_TEMPLATE_ID>.

  • config object

    Per-kind settings. rescanWindowSeconds (number) dedups scans — a pass re-scanned within the window on any of this application's readers is rejected as rejected_replay. For forward, endpointUrl (https) is required. Optional onSuccess / onFailure objects override the reader instruction (led, beep, message).

curl -X POST 'https://api.passninja.com/v1/applications' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>' \ -H 'Content-Type: application/json' \ -d '{ "name": "Front gate validation", "kind": "validate", "passTemplate": "<YOUR_TEMPLATE_ID>", "config": { "rescanWindowSeconds": 14400 } }'

Returns 201 with the created application:

{ "id": "app_0x1", "name": "Front gate validation", "description": null, "kind": "validate", "passTemplate": "ptk_0x216", "config": { "rescanWindowSeconds": 14400 }, "active": true, "createdAt": "2026-07-24T18:00:00Z", "updatedAt": "2026-07-24T18:00:00Z" }

List applications Premium

List every application on your account. GET /v1/applications/<APPLICATION_ID> returns a single application.

curl -X GET 'https://api.passninja.com/v1/applications' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'
{ "applications": [ { "id": "app_0x1", "name": "Front gate validation", "kind": "validate", "passTemplate": "ptk_0x216", "active": true, "readerCount": 2 } ] }

Update an application Premium

Update name, description, kind, config, or active. The pass-template binding is immutable. Changing to validate / forward requires the corresponding entitlement.

curl -X PATCH 'https://api.passninja.com/v1/applications/<APPLICATION_ID>' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>' \ -H 'Content-Type: application/json' \ -d '{ "config": { "rescanWindowSeconds": 7200 } }'

Delete an application Premium

Delete an application. Returns 409 if it is still bound to any reader — unbind it first.

curl -X DELETE 'https://api.passninja.com/v1/applications/<APPLICATION_ID>' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

Readers Premium

A reader is a registered NFC reader on your account. A reader binds to one or more applications, and the templates it can scan derive from those applications (at most 3 Apple-template and 3 Google-template applications per reader). Each reader is issued a bearer token at creation, shown exactly once, which the physical reader uses to authenticate scan, heartbeat, and pre-sign calls — never your account API key.

Binding invariant: a template must have zero live issued passes when it is first paired with a reader, because from that point on every pass issued from it carries its serial as the NFC identity so scans can be attributed. Reader creation is billed like a pass template unless the account holds the unlimited-readers entitlement.

Terminal output: if you're using one of our reader terminals, you may be wondering what the output stream from the terminal is. Our readers emulate a keyboard device and stream the NFC data to stdin, encoded in ASCII format.

Create a reader Premium

Register a reader. Only name, location, and applications are required — the hardware identity (serial, model, firmware) is reported later via heartbeat, not at creation. The response includes the bearer token exactly once; store it immediately.

  • name* string

    Human label, e.g. Front gate.

  • location* string

    Free-text location for the reader.

  • applications* array

    One or more application ids (app_0x...). Each is validated against the 3-Apple / 3-Google binding limit and the zero-live-passes invariant; a failure names the offending template.

curl -X POST 'https://api.passninja.com/v1/readers' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>' \ -H 'Content-Type: application/json' \ -d '{ "name": "Front gate", "location": "Torreón - Puerta 2", "applications": ["app_0x1"] }'

Returns 201. The token is shown only here:

{ "id": "1", "name": "Front gate", "location": "Torreón - Puerta 2", "status": "active", "applications": [ { "id": "app_0x1", "name": "Front gate validation", "kind": "validate", "passTemplate": "ptk_0x216" } ], "serial": null, "token": "rdr_e159d817f592d22368728b93763780ffd2bf978b55654aa1" }

List readers Premium

List your readers. GET /v1/readers/<READER_ID> returns a single reader with its health, hardware identity, application bindings, and last-seen time.

curl -X GET 'https://api.passninja.com/v1/readers' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

Update a reader Premium

Update name, location, status (active / revoked), or replace the applications binding set. Newly added bindings run the same validation as creation; applications already bound stay exempt from the zero-live-passes rule.

curl -X PATCH 'https://api.passninja.com/v1/readers/<READER_ID>' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>' \ -H 'Content-Type: application/json' \ -d '{ "location": "Torreón - Puerta 3" }'

Rotate reader token Premium

Mint a new bearer token for a reader; the old token stops working immediately. The new token is returned exactly once. DELETE /v1/readers/<READER_ID> removes a reader entirely.

curl -X POST 'https://api.passninja.com/v1/readers/<READER_ID>/token' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'

Get merged reader config Premium

Return the reader-config values (Apple merchant id / Google collector id + key version and their decryption keys) merged across all of the reader's bound templates — what a CLI client or provisioning flow loads onto the hardware. Bounded by the 3-Apple / 3-Google binding limit.

curl -X GET 'https://api.passninja.com/v1/readers/<READER_ID>/config' \ -H 'X-API-KEY: <YOUR_API_KEY>' \ -H 'X-ACCOUNT-ID: <YOUR_ACCOUNT_ID>'
{ "id": "1", "templates": [ { "id": "ptk_0x216", "platform": "apple", "apple": { "vas_merchant_id": "pass.com.example", "vas_private_key_pem": "-----BEGIN EC PRIVATE KEY-----..." }, "google": null } ] }

Scans Premium

Scan endpoints are called by the reader itself and authenticate with the reader's bearer tokenAuthorization: Bearer <READER_TOKEN> — never the account API key. A scan resolves the captured value to an issued pass on one of the reader's bound templates, applies the bound application's policy, and returns an LED/beep instruction in the same response. A pn.pass.scanned webhook fires for each recorded scan.

Record a scan Premium

Readers that decrypt on-device send the plaintext message; readers that forward encrypted data send the raw payload (plus a session block for Google Smart Tap) and the server decrypts it.

  • scanType string

    Currently nfc (default).

  • message string

    The decrypted value (the pass serial) when the reader decrypts locally. Provide this or payload.

  • payload string

    Hex-encoded captured data for server-side decryption. Pair with platform and, for Google, session.

  • platform string

    apple or google, when a raw payload is sent.

  • session object

    Required for a Google payload — the same Smart Tap session block as the decrypt endpoint.

curl -X POST 'https://api.passninja.com/v1/scans' \ -H 'Authorization: Bearer <READER_TOKEN>' \ -H 'Content-Type: application/json' \ -d '{ "scanType": "nfc", "platform": "apple", "message": "fee4a257185906b92a" }'

Returns 200 with the result and the reader instructions. Results are accepted, rejected_inactive, or rejected_replay; a value that resolves to no issued pass returns unresolved and is not persisted:

{ "scanId": "9f8e7d6c-1a2b-4c3d-8e4f-5a6b7c8d9e0f", "result": "accepted", "pass": { "passTemplate": "ptk_0x216", "passId": "fee4a257185906b92a" }, "readerInstructions": { "success": true, "led": "green", "beep": true, "message": "Accepted" } }

Reader heartbeat Premium

Readers with no vendor MDM (source cli-client or api) report their hardware identity and liveness here. Reader-token authed, and gated by the scan-events-heartbeat entitlement. Send any of serial, manufacturer, model, firmware, source, and params.

curl -X POST 'https://api.passninja.com/v1/readers/heartbeat' \ -H 'Authorization: Bearer <READER_TOKEN>' \ -H 'Content-Type: application/json' \ -d '{ "serial": "AJ00217043", "manufacturer": "Dot Origin", "model": "VTAP100-PRO-POE", "firmware": "2.10.1", "source": "cli-client" }'

Pre-sign a Smart Tap session Premium

For Google Smart Tap, the server generates the terminal nonce and ephemeral key pair and signs the session with the long-term collector key, so the reader never holds that key. Reader-token authed; the reader must be paired with the requested template (403 otherwise). The reader completes the tap, then sends the returned session context back with the scan (or decrypt) call.

curl -X POST 'https://api.passninja.com/v1/passes/<YOUR_TEMPLATE_ID>/smarttap/presign' \ -H 'Authorization: Bearer <READER_TOKEN>'
{ "terminalNonce": "<HEX>", "terminalEphemeralPublicKey": "<HEX>", "terminalEphemeralPrivateKey": "<HEX>", "signature": "<HEX>", "keyVersion": "1" }

Errors: Standard codes

We return a standard set of error codes, in addition to standard HTTP status codes. You can find the full list below:

Error code HTTP Status Meaning
UA1 401 Unauthorized request - you are missing a required header. Please review the Authentication section.
MP1 402 Missing credits - you are missing credits to use the API.
UA2 403 Forbidden - you are attempting to access someone elses passes - please stop.
RL1 429 Rate limit exceeded - you are sending too many requests, perhaps you are being throttled by adding recipients to pass creation. You may want to upgrade to a premium plan.
MR1 404 Pass template does not exist - you are attempting to use a pass template that does not exist.
MR2 404 Pass does not exist - you are attempting to access a pass that does not exist - it may have been destroyed.
MI1 400 Invalid pass body - The keys you are using for this pass template or pass do not fit the schema you defined in the PassNinja developer console.
IP1 400 Incompatible platform - The payload you are trying to decrypt is not decryptable because the pass is not for Apple.
AL1 400 Un-installable Apple pass - The pass would set an appLaunchURL without an accompanying app identifier (associatedStoreIdentifiers or systemAppBundleIdentifiers), which Apple Wallet refuses to install ("Safari cannot download this file"). Set an app identifier or clear the launch URL.
GE77 500 Generic Error - Sometimes things go wrong, we're sorry and we will investigate.