> For the complete documentation index, see [llms.txt](https://docs.thewalletcrew.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.thewalletcrew.io/connectors/custom-connector/installation-changed-extensibility.md).

# Pass installation and uninstallation hooks

Create a custom connector that calls an external API when a pass is installed or uninstalled in Apple Wallet or Google Wallet.

It is possible to create a **custom connector** in **The Wallet Crew** to call an external API whenever a pass is **installed** or **uninstalled** on Apple Wallet or Google Wallet.

This allows brands to **synchronize installation status** with external systems such as a CRM, analytics tool, or marketing automation platform.

<details>

<summary>Real-world examples</summary>

* A retail brand updates a CRM field like `hasWalletPass=true` right after installation. It then stops sending “add to wallet” reminders.
* A ticketing brand logs installs and uninstalls in its BI stack. It monitors adoption by event, channel, and device type.
* A brand triggers a welcome journey after installation. It uses `registrationSource` to segment by UTM campaign.
* A brand detects unusual uninstall spikes after a bad release. It rolls back quickly and communicates proactively.

</details>

### Runtime Hooks

The platform exposes the following hooks on the `runtime.wallet.passUpdater` endpoint:

* **`OnPassInstalled`** → Triggered when a pass is added to Apple/Google Wallet
* **`OnPassUninstalled`** → Triggered when a pass is removed from Apple/Google Wallet

The same `runtime.wallet.passUpdater` registration also exposes four hooks around pass creation and update — `OnPassCreating`, `OnPassCreated`, `OnPassUpdating`, `OnPassUpdated`. They are not covered on this page, which focuses on the install/uninstall pair. See [Custom connectors → What this registration does](/developers-guides/integration-guides/custom-connectors.md) for the full set.

Both methods receive the same parameters:

| Parameter               | Type                                    | Description                                                   |
| ----------------------- | --------------------------------------- | ------------------------------------------------------------- |
| `passId`                | `string`                                | Unique identifier of the pass                                 |
| `passType`              | `string`                                | Type of pass (loyalty, gift card, event ticket…)              |
| `identifiers`           | `Record<string, any>`                   | Key/value identifiers defined on the pass (e.g. `customerId`) |
| `device`                | `"apple"` or `"google"`                 | Wallet platform                                               |
| `additionalInformation` | `AdditionalPassInstallationInformation` | Includes registration statistics                              |

### Example Implementation

Scripts can be placed in the `server/scripts` folder under advanced configuration.

```js
const API_URL = 'https://partner';
import { getSecret } from 'neo/secrets';

/** 
 * @typedef {Object} RegistrationInformation 
 * @property {number} activeRegistrationCount - Active registrations. 
 * @property {number} totalRegistrationCount - Total registrations. 
 **/ 

/**
 * @typedef {Object} RegistrationSource
 * @property {string[]} tags - list of tags specified by the SDK integration
 * @property {string} medium - medium specified by the SDK integration or utm_medium
 * @property {string} origin  - url from where the pass is installed - could be overriden by the SDK integration
 * @property {string} userAgent - optional UserAgent used to install the pass
 */

/** 
 * @typedef {Object} AdditionalPassInstallationInformation
 * @property {RegistrationInformation} registrationInformation - Registration statistics. 
 * @property {RegistrationSource} registrationSource - Source of the installation when available 
 */

/** methods triggered when a pass is installed 
 * @param {string} passId - unique identifier of the pass
 * @param {string} passType - pass type of the current pass
 * @param {Record<string, any>} identifiers - key value pairs of identifiers of the pass
 * @param {"apple"|"google"} device - named of the device
 * @param {AdditionalPassInstallationInformation} additionalInformation - additionalInformation related to this pass installation */
 async function onPassInstalled(passId, passType, identifiers, device, additionalInformation) {
     await onPassInstallationStatusChanged(passId, passType, identifiers, device, true);
} 

/** methods triggered when a pass is uninstalled 
 * @param {string} passId - unique identifier of the pass 
 * @param {string} passType - pass type of the current pass
 * @param {Record<string, any>} identifiers - key value pairs of identifiers of the pass
 * @param {"apple"|"google"} device - named of the device 
 * @param {AdditionalPassInstallationInformation} additionalInformation - additionalInformation related to this pass installation 
 */ 
async function onPassUninstalled(passId, passType, identifiers, device, additionalInformation) { 
    await onPassInstallationStatusChanged(passId, passType, identifiers, device, false); 
} 

async function onPassInstallationStatusChanged(passId, passType, identifiers, device, isInstalled) {
  const customerId = identifiers["customerId"]; // or any other external identifier
  const API_SECRET = await getSecret('PARTNER-API-SECRET');

  // ⚠️ Note: fetch is not standard here. It uses PascalCase options and object-based Body.
  await fetch(API_URL + '/wallet/installation', {
    Method: "POST",
    Headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      "X-API-KEY": API_SECRET
    },
    Body: {
      customerId,
      isInstalled,
      device
    },
    ThrowOnError: false
  });
}

export default function (context) {
  context.register('runtime.wallet.passUpdater', {
    OnPassInstalled: onPassInstalled,
    OnPassUninstalled: onPassUninstalled
  });
}
```

### Minimal Example

For testing or logging only:

```js
async function onPassInstalled(passId, passType, identifiers, device) {
  console.log(`Pass ${passId} installed on ${device}`);
}

async function onPassUninstalled(passId, passType, identifiers, device) {
  console.log(`Pass ${passId} uninstalled from ${device}`);
}

export default function (context) {
  context.register('runtime.wallet.passUpdater', {
    OnPassInstalled: onPassInstalled,
    OnPassUninstalled: onPassUninstalled
  });
}
```

### When Are Events Triggered?

* **`OnPassInstalled`** → fired when a customer successfully adds a pass to Apple or Google Wallet.
* **`OnPassUninstalled`** → fired when a customer removes the pass from their wallet.
* Events are reliable: the platform ensures correct delivery even under high load.

### Notes

* `fetch` is **not the standard browser fetch** — it accepts PascalCase options (`Method`, `Headers`, `Body`, `ThrowOnError`).
* Authentication is flexible: API keys (via `getSecret`) or OAuth with the custom `fetch` are both supported.
* Execution runs inside the same sandboxed runtime as every custom connector script: a 5-second execution limit per invocation, a restricted set of approved libraries, and automated validation before a script can be saved. See [Custom connectors → Runtime constraints](/developers-guides/integration-guides/custom-connectors.md) for the full list of what is available inside a script.

### FAQ

<details>

<summary>Are these events real-time?</summary>

They trigger when the pass is added or removed in the wallet. In practice, treat them as near real-time. Design the receiving API to be idempotent, so retries do not create duplicates.

</details>

<details>

<summary>What happens if the external API is down?</summary>

The connector code controls the call. Handle timeouts and errors gracefully. If the external system is critical, implement retries and a dead-letter strategy on the receiving side.

</details>

<details>

<summary>Can this be used to trigger emails or push campaigns?</summary>

Yes. The most common pattern is to call the marketing automation platform (directly or through the brand backend) and trigger a journey on install. Use uninstall events to stop or adapt communications.

</details>

<details>

<summary>What is included in <code>additionalInformation</code>?</summary>

It can include registration statistics and, when available, the installation source. Use it to measure adoption and attribute installs to a campaign or SDK integration.

</details>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.thewalletcrew.io/connectors/custom-connector/installation-changed-extensibility.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
