Update a Pass from a Klaviyo Flow

Daniel Baudino

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

RequirementDetails
PassNinja accountCustomer must already have a pass
Klaviyo integration connectedPassNinja → Integrations → Klaviyo must be connected
external_id on profileSet automatically to the pass serial after pass creation

You will need:

ValueWhere to find it
PASSNINJA_API_KEYPassNinja → Settings → API Keys
PASSNINJA_ACCOUNT_IDPassNinja → Settings → API Keys (format: aid_0x...)
PASSNINJA_TEMPLATE_IDPassNinja → 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_id is set

This prevents the webhook from firing for profiles that haven't been issued a pass yet.

Step 2 — Configure the request

FieldValue
MethodPATCH
URLhttps://api.passninja.com/v1/passes/ptk_0x4d5e6f/{{ person.external_id }}

Replace ptk_0x4d5e6f with your actual template ID.

Step 3 — Add headers

HeaderValue
X-API-KEYYour PassNinja API key
X-ACCOUNT-IDYour account ID (e.g. aid_0x1a2b3c)
Content-Typeapplication/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

TriggerUse case
Custom metric: Points EarnedUpdate balance after a transaction
Custom metric: Tier ChangedReflect new tier on the pass
Custom metric: Reward RedeemedDeduct 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:

VariableValue
PASSNINJA_API_KEYYour PassNinja API key
PASSNINJA_ACCOUNT_IDYour account ID
PASSNINJA_TEMPLATE_IDYour 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

TriggerUse case
Custom metric: Points EarnedUpdate balance after a transaction
Custom metric: Tier ChangedReflect new tier on the pass
Custom metric: Reward RedeemedDeduct 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 set condition before the action
  • Code block: The if (!serial) guard already handles this — it returns skipped instead 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:

  1. Confirm the update returned a 2xx response in Klaviyo's activity log or Code output
  2. Check that the device has an active internet connection
  3. Apple Wallet refreshes may take a few seconds

Support

Was this article helpful?
Yes No