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.
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.
•
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": "storeCard",
"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* stringThe pass template identifier, present in the Get Passes tab for a given pass template. It looks like this:
<YOUR_TEMPLATE_ID>
•
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": "storeCard",
"issuedPassCount": 150,
"installedPassCount": 89
}
Create a pass template Enterprise
Programmatically provision a new pass template under your account. The created template starts empty — define its visible fields, art, and styling through the pass template editor in the dashboard before issuing passes against it.
Below is the list of acceptable params for creating a pass template. * denotes a required parameter:
-
name* stringHuman label for the template, ≤ 120 characters.
-
platform* stringOne of
apple,google, orboth. -
style* stringPass style identifier (e.g.
storeCard,eventTicket,coupon,boardingPass,generic). Must be valid for the chosenplatform— whenplatformisboth, the style must exist in the Apple and Google catalogs. See Platform Parameters for the full list.
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": "storeCard"
}'
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": "storeCard",
"issuedPassCount": 0,
"installedPassCount": 0,
"createdAt": "2026-05-12T18:00:00Z",
"updatedAt": "2026-05-12T18:00:00Z"
}
Delete a pass template Enterprise
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* stringThe pass template identifier — looks like
<YOUR_TEMPLATE_ID>. Must belong to the calling account; otherwise404is 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.
For self-serve customers, if you send text messages you will be rate limited to a maximum of 10 requests per second. For enterprise 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* stringThis 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* objectThe 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) objectThe 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* stringThe name of the person receiving the pass.
-
message(optional) stringThe 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) stringInternationally formatted phone number to send message to, with a + in front. For example:
+15613437899 -
email(optional) stringRFC 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* stringThis 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> -
pageinteger (optional)Page number for pagination. When provided, the response includes a
metaobject with pagination details. Without this parameter, all passes are returned. -
per_pageinteger (optional)Number of passes per page. Defaults to 10. Only applies when
pageis provided. -
searchstring (optional)Search passes by serial number. Performs a case-insensitive partial match.
•
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): current_page, total_pages, total_count, per_page
curl -X GET 'https://api.passninja.com/v1/passes/<YOUR_TEMPLATE_ID>?page=1&per_page=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": {
"current_page": 1,
"total_pages": 5,
"total_count": 42,
"per_page": 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* stringThis 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* stringPassId also known as a serial number. Takes on a namespaced format which looks like
<SERIAL_NUMBER>
•
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"
}
}
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.
Below is a list of required params for decrypting a pass:
-
passTemplate* stringThis 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> -
payload* stringPayload is a hex encoded APDUs with no spaces. It looks like this (abbreviated for presentation):
55166a97002...1ea070f0d4fe88887
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 '{
"payload": "55166a9700250a8c51382dd16822b0c763136090b91099c16385f2961b7d9392d31b386cae133dca1b2faf10e93a1f8f26343ef56c4b35d5bf6cb8cd9ff45177e1ea070f0d4fe88887"
}'
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* stringThis 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* stringPassId also known as a serial number. Takes on a namespaced format which looks like
<SERIAL_NUMBER> -
pass* objectThe 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.
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* stringThis 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* stringPassId also known as a serial number. Takes on a namespaced format which looks like
<SERIAL_NUMBER> -
pass* objectThe 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* stringThis 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* stringPassId 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 Enterprise
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 an enterprise 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.
- 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.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.scanned |
Reserved | Reserved for a future release. Subscriptions are accepted; deliveries are not yet emitted. |
pn.pass.expired |
Reserved | Reserved for a future release. |
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 (CloudEvents 1.0)
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": {
"pass_template": "ptk_0x216",
"pass_id": "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 |
pass_template |
string | Pass template API key, e.g. ptk_0x216. |
pass_id |
string | Pass serial number (the ex_id 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
Authorizationheader matches the bearer token returned at subscription creation. Reject anything else with HTTP 401. - Respond with any
2xxstatus 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
idattribute as the dedupe key. Retries (and rare delivery duplicates) reuse the sameid;pnattemptchanges 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. |
subscribed_events |
yes | Non-empty array of CloudEvents type strings (see Event types above). |
auth_method |
no | Defaults to "bearer_token". "mtls" is reserved (returns HTTP 501 today). |
pass_template |
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",
"subscribed_events": ["pn.pass.installed", "pn.pass.uninstalled"]
}'
Example 201 response — note that bearer_token appears here and nowhere else:
{
"id": "47",
"name": "Production install handler",
"url": "https://example.com/passninja/webhook",
"auth_method": "bearer_token",
"subscribed_events": ["pn.pass.installed", "pn.pass.uninstalled"],
"pass_template": null,
"active": true,
"created_at": "2026-05-12T18:00:00.000Z",
"updated_at": "2026-05-12T18:00:00.000Z",
"bearer_token": "wbk_b3d6…"
}
List webhook subscriptions
GET /v1/webhooks
Returns subscriptions in the calling account, newest first. Supports query params page (default 1), per_page (default 50, max 100), and pass_template=ptk_0x... to filter by scope.
curl -X GET 'https://api.passninja.com/v1/webhooks?per_page=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 webhook_results row. Useful for diagnosing receiver-side outages without grepping your own logs. Same pagination params as the list endpoint.
{
"webhook_results": [
{
"id": "1820",
"webhook_id": "47",
"url": "https://example.com/passninja/webhook",
"response_status": 200,
"response_body": "{\"ok\":true}",
"success": true,
"attempt": 1,
"created_at": "2026-05-12T18:25:43.522Z",
"updated_at": "2026-05-12T18:25:43.522Z"
}
],
"page": 1,
"per_page": 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
2xxresponse from your endpoint. - Timeout: requests time out after 10 seconds and count as a failure.
- Stable identity: retries reuse the same CloudEvents
idanddatapayload; onlypnattemptincrements. - 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 accumulate noise in
/v1/webhooks/<id>/results.
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.
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 an enterprise 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. |
| GE77 | 500 | Generic Error - Sometimes things go wrong, we're sorry and we will investigate. |
Terminal: Simple readers
If you're using one of our reader terminals, then 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.