Update a Pass from a Klaviyo Flow
Updated June 3, 2026
TL;DR: Automatically update pass content when Klaviyo profile properties change — points balance, tier, expiry, and more.
- Two methods: Webhook action (no code) or Code block (full control)
- The pass serial number is stored on the Klaviyo profile automatically after creation
- Guard against profiles without a pass before firing the update
- PassNinja pushes the updated pass to the customer's wallet instantly
Overview
Once a customer has a pass, you can update its content whenever their Klaviyo profile changes — points earned, tier upgraded, reward redeemed, expiry extended. The update call uses the pass serial number stored on the profile by PassNinja after the pass was created.
Prerequisites
| Requirement | Details |
|---|---|
| PassNinja account | Customer must already have a pass |
| Klaviyo integration connected | PassNinja → Integrations → Klaviyo must be connected |
external_id on profile | Set automatically to the pass serial after pass creation |
You will need:
| Value | Where to find it |
|---|---|
PASSNINJA_API_KEY | PassNinja → Settings → API Keys |
PASSNINJA_ACCOUNT_ID | PassNinja → Settings → API Keys (format: aid_0x...) |
PASSNINJA_TEMPLATE_ID | PassNinja → Pass Templates → select template (format: ptk_0x...) |
Method 1: Webhook Action (No Code)
Step 1 — Guard against profiles without a pass
Before the Webhook action, add a Profile Property condition:
external_idis set
This prevents the webhook from firing for profiles that haven't been issued a pass yet.
Step 2 — Configure the request
| Field | Value |
|---|---|
| Method | PATCH |
| URL | https://api.passninja.com/v1/passes/ptk_0x4d5e6f/{{ person.external_id }} |
Replace ptk_0x4d5e6f with your actual template ID.
Step 3 — Add headers
| Header | Value |
|---|---|
X-API-KEY | Your PassNinja API key |
X-ACCOUNT-ID | Your account ID (e.g. aid_0x1a2b3c) |
Content-Type | application/json |
Step 4 — Set the request body
{
"pass": {
"points-balance": "{{ person.properties.loyalty_points|default:0 }}",
"loyalty-tier": "{{ person.properties.loyalty_tier|default:'Standard' }}"
}
}
Only include the fields you want to change. Fields not listed in the body are left as-is on the pass.
Suggested triggers
| Trigger | Use case |
|---|---|
| Custom metric: Points Earned | Update balance after a transaction |
| Custom metric: Tier Changed | Reflect new tier on the pass |
| Custom metric: Reward Redeemed | Deduct points after redemption |
| Profile Updated (with filter) | Catch-all for profile property changes |
Method 2: Code Block
Step 1 — Set up environment variables
In the Code action's Environment Variables tab, add:
| Variable | Value |
|---|---|
PASSNINJA_API_KEY | Your PassNinja API key |
PASSNINJA_ACCOUNT_ID | Your account ID |
PASSNINJA_TEMPLATE_ID | Your pass template ID |
Step 2 — Write the code
export const handler = async (event, profile, context) => {
const attrs = profile.data.attributes;
const serial = attrs.external_id;
if (!serial) {
return { skipped: true, reason: 'no active pass' };
}
const response = await fetch(
`https://api.passninja.com/v1/passes/${process.env.PASSNINJA_TEMPLATE_ID}/${serial}`,
{
method: 'PATCH',
headers: {
'X-API-KEY': process.env.PASSNINJA_API_KEY,
'X-ACCOUNT-ID': process.env.PASSNINJA_ACCOUNT_ID,
'Content-Type': 'application/json',
},
body: JSON.stringify({
pass: {
'points-balance': String(attrs.properties?.loyalty_points ?? 0),
'loyalty-tier': attrs.properties?.loyalty_tier ?? 'Standard',
},
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`PassNinja error: ${JSON.stringify(error)}`);
}
return { updated: true };
};
Replace the keys inside pass with the api_field_name values you want to update — find these in PassNinja → Pass Templates → Fields.
Suggested triggers
| Trigger | Use case |
|---|---|
| Custom metric: Points Earned | Update balance after a transaction |
| Custom metric: Tier Changed | Reflect new tier on the pass |
| Custom metric: Reward Redeemed | Deduct points after redemption |
| Profile Updated (with filter) | Catch-all for profile property changes |
Troubleshooting
Update fails with 404
The serial stored as external_id may belong to a pass that was already deleted.
- Webhook: Check the
external_id is setcondition before the action - Code block: The
if (!serial)guard already handles this — it returnsskippedinstead of throwing
Pass not refreshing in wallet
PassNinja sends an APN/FCM push notification to the device after every update. If the wallet doesn't refresh:
- Confirm the update returned a 2xx response in Klaviyo's activity log or Code output
- Check that the device has an active internet connection
- Apple Wallet refreshes may take a few seconds
Support
- Email: support@passninja.com
- API Reference: passninja.com/documentation
- All Klaviyo tutorials: passninja.com/tutorials/klaviyo
More articles focused on Klaviyo
PassNinja's native Klaviyo integration brings four capabilities to your marketing stack: NFC-enab...
Create a Pass from a Klaviyo Flow Using a Webhook ActionKlaviyo's native **Webhook** flow action sends an HTTP POST directly to the PassNinja API. You co...
Delete a Pass from a Klaviyo FlowUse a delete flow to revoke a customer's pass when they churn, unsubscribe globally, or their mem...
Getting Started with KlaviyoWith PassNinja + Klaviyo, you can: