> 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/developers-guides/integration-guides/webhooks.md).

# Webhooks

Receive real-time events from The Wallet Crew via HTTPS webhooks. Create endpoints, validate signatures, and handle common event payloads.

## Webhooks

Webhooks let external systems receive real-time notifications when something happens in The Wallet Crew. Register an HTTPS endpoint, select events, and The Wallet Crew sends a `POST` request each time one of those events occurs.

This is the simplest way to keep your CRM, analytics, or operational systems in sync without polling APIs.

<details>

<summary><strong>Real-world examples</strong></summary>

* Send a “pass installed” event to your CRM to measure adoption per campaign.
* Trigger a customer welcome journey when a customer profile is upserted.
* Log pass lifecycle events (`Pass:Created`, `Pass:Updated`) to your data warehouse.
* Track in-store QR usage by listening to redirect events (`Redirect:Redirected`).

</details>

{% hint style="info" %}
If you also enforce network restrictions, you can allowlist The Wallet Crew outgoing IPs. Do not rely on IPs only. Always validate `x-neostore-signature`.

See [Infrastructure](/developers-guides/pass-architecture/infrastructure.md).
{% endhint %}

### Create and manage a webhook

Webhooks can be created and managed in two ways: from the admin console (no API required) or via the REST API.

**Admin console (self-serve):** Go to **Settings → General → Webhooks**. Create or edit a webhook by filling in the **Description**, **Endpoint**, and **Events** fields. The console generates an ID and a Secret (HMAC signing key) automatically. Copy the Secret immediately after creation — it is not shown again in full. See [Webhooks (Configure)](/developers-guides/integration-guides/webhooks/webhooks-configure.md) for the full console reference.

**REST API:** Use the operation below when webhook registration needs automation in a deployment pipeline. A webhook defines three things: where to send requests, which events to send, and whether the webhook is enabled.

### Create a webhook (API)

When creating a webhook, send:

* `endpoint`: the HTTPS URL that will receive `POST` requests.
* `events`: the events you want to subscribe to.
* `enabled`: whether delivery is active.

You can subscribe to multiple events in one webhook. You can also use `*` to subscribe to all sub-events under a category.

{% hint style="warning" %}
Treat `signatureSecret` like a password. Store it securely. Use it only server-side.
{% endhint %}

## Creates a webhook subscription. The platform generates its identifier and signature secret.

> The subscription applies from the next matching event. The response carries the generated \`id\`\
> and \`signatureSecret\`; the \`Location\` header points to the subscription.

```json
{"openapi":"3.1.1","info":{"title":"Neostore internal API","version":"v1"},"tags":[{"name":"WebHook"}],"servers":[{"url":"https://app.neostore.cloud","description":"Production Server"},{"url":"https://app-qa.neostore.cloud","description":"Staging Server"}],"security":[{"admin-bearer":["tenant.webhook:write"]},{"apiKey":[]}],"components":{"securitySchemes":{"admin-bearer":{"type":"oauth2","flows":{"implicit":{"authorizationUrl":"https://auth.neostore.cloud/authorize?audience=https://app.neostore.cloud/api/","scopes":{}}}},"apiKey":{"type":"apiKey","name":"X-API-KEY","in":"header"}},"schemas":{"WebHook":{"required":["endpoint","events"],"type":"object","properties":{"description":{"type":["null","string"],"description":"Free text to tell subscriptions apart; not used by the platform. Omitted on an update, the current\ndescription is cleared."},"events":{"minItems":1,"type":"array","items":{"type":"string"},"description":"Event names that trigger a delivery. An entry matches its exact event name, or every event name\nstarting with what precedes a trailing `*` (`pass.*`; `*` alone matches every event).\nA `*` anywhere else is taken literally."},"endpoint":{"type":"string","description":"URL that receives each matching event as a JSON `POST`, with the headers\n`X-NEOSTORE-TENANTID`, `X-NEOSTORE-EVENTNAME` and `X-NEOSTORE-SIGNATURE`.","format":"uri"},"enabled":{"type":"boolean","description":"Whether events are delivered. A disabled subscription is kept but receives nothing. Defaults to\n`false`, including when omitted on an update.","default":false}},"additionalProperties":false,"description":"The caller-editable part of a webhook subscription."},"WebHookWithId":{"required":["id","signatureSecret"],"type":"object","allOf":[{"description":"The caller-editable part of a webhook subscription.","$ref":"#/components/schemas/WebHook"}],"properties":{"id":{"minLength":1,"type":"string","description":"Identifier the platform assigns on creation. It never changes and is the `id` the other\nwebhook routes take."},"signatureSecret":{"minLength":1,"type":"string","description":"Secret the platform generates on creation to sign every delivery.\nIt never changes, not even on an update."}},"additionalProperties":false,"description":"A webhook subscription as stored, with the values the platform generated for it."},"ProblemDetails":{"type":"object","properties":{"type":{"type":["null","string"]},"title":{"type":["null","string"]},"status":{"type":["null","integer"],"format":"int32"},"detail":{"type":["null","string"]},"instance":{"type":["null","string"]}},"additionalProperties":{}},"HttpValidationProblemDetails":{"type":"object","allOf":[{"$ref":"#/components/schemas/ProblemDetails"}],"properties":{"errors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}},"additionalProperties":{}}}},"paths":{"/api/{tenantId}/webhooks":{"post":{"tags":["WebHook"],"summary":"Creates a webhook subscription. The platform generates its identifier and signature secret.","description":"The subscription applies from the next matching event. The response carries the generated `id`\nand `signatureSecret`; the `Location` header points to the subscription.","parameters":[{"name":"tenantId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Events to listen to, the endpoint to call and whether delivery is enabled.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/WebHook"},{"$ref":"#/components/schemas/WebHookWithId"}],"description":"The caller-editable part of a webhook subscription."}},"text/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/WebHook"},{"$ref":"#/components/schemas/WebHookWithId"}],"description":"The caller-editable part of a webhook subscription."}},"application/*+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/WebHook"},{"$ref":"#/components/schemas/WebHookWithId"}],"description":"The caller-editable part of a webhook subscription."}}},"required":true},"responses":{"201":{"description":"Subscription created.","content":{"text/plain":{"schema":{"description":"A webhook subscription as stored, with the values the platform generated for it.","$ref":"#/components/schemas/WebHookWithId"}},"application/json":{"schema":{"description":"A webhook subscription as stored, with the values the platform generated for it.","$ref":"#/components/schemas/WebHookWithId"}},"text/json":{"schema":{"description":"A webhook subscription as stored, with the values the platform generated for it.","$ref":"#/components/schemas/WebHookWithId"}}}},"400":{"description":"Body missing, `events` empty or missing, or `endpoint` missing or not an absolute `http` or `https` URL.","content":{"text/plain":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"text/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"401":{"description":"The caller is not authenticated.","content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"403":{"description":"The caller lacks the <c>tenant.webhook:write</c> scope, or the endpoint is not enabled for the tenant.","content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"404":{"description":"The tenant does not exist.","content":{"text/plain":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"text/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"429":{"description":"Over 1200 requests per minute to this endpoint for the tenant (default limit); retry after <c>Retry-After</c>.","headers":{"Retry-After":{"description":"Seconds to wait before retrying. Sent when the limiter knows it.","schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}}}}}}}
```

The response includes the webhook `id` and `signatureSecret`.

### Update, list, and delete

You can manage webhooks using `GET`, `PATCH`, and `DELETE` on the same resource.

For the full API definition, use the [API reference](https://docs.thewalletcrew.io/api-reference/).

<div data-with-frame="true"><figure><img src="https://4179171865-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOnaDC4sjKAx53j0QV843%2Fuploads%2F0WWczmIeUHda36SFDsIS%2Fdevelop-webhook-webhook-screen-showing-event-subscriptions-endpoint-url.png?alt=media&amp;token=b8551078-70ee-4cf9-b183-d8b50905d118" alt="Webhook configuration screen showing event subscriptions and endpoint URL."><figcaption><p>Configure which events are delivered to which endpoint.</p></figcaption></figure></div>

### What The Wallet Crew sends

Each webhook delivery is an HTTP `POST` request with headers and a JSON body. The body depends on the event type. Every payload includes the event metadata fields prefixed with `__`.

### HTTP headers

* `x-neostore-signature`: HMAC SHA-256 signature of the request body, generated using your `signatureSecret`.
* `x-neostore-eventname`: event name that triggered the webhook (example: `Customer:Upserted`).
* `x-neostore-tenantid`: tenant identifier in The Wallet Crew.

### How to process events reliably

Use `__id` as an idempotency key. If your endpoint receives the same payload twice, you can safely ignore the duplicate.

Keep your handler fast. A common pattern is to validate the signature, enqueue the event internally, then return `2xx`.

### Verify webhook authenticity

Validate every webhook request using `x-neostore-signature`. This ensures the request body was sent by The Wallet Crew and was not modified in transit.

To validate it, compute an HMAC SHA-256 of the raw request body using your `signatureSecret`, then compare it with the header value.

{% hint style="warning" %}
Signature validation must use the exact raw body bytes you received. Do not re-serialize JSON before hashing.
{% endhint %}

### Common events

The event list evolves. Use the [API reference](https://docs.thewalletcrew.io/api-reference/) as the source of truth for event names and payload shapes.

Below are the most common events teams integrate with.

<details>

<summary><strong>Customer events</strong></summary>

`Customer:Upserted` is sent when a customer is created or updated.

</details>

<details>

<summary><strong>Pass lifecycle events</strong></summary>

Typical pass lifecycle events:

* `Pass:Created`
* `Pass:Installed`
* `Pass:Uninstalled`
* `Pass:Updated`
* `Pass:UpdateSent`
* `Pass:ThemeChanged`

`Pass:Installed` includes device fields. `Pass:ThemeChanged` is sent when a pass moves onto a different theme or has its theme removed, and carries the theme before and after the change.

Pass events carry no actor: no user and no API key. They describe what happened to the pass, not who caused it.

</details>

<details>

<summary><strong>Theme events</strong></summary>

`Theme:Created`, `Theme:Updated` and `Theme:Deleted` are sent when a theme is added, saved or removed. Each carries two objects:

* `theme` — `themeId`, `name` and `tags`. On `Theme:Deleted`, these describe the theme as it was before deletion; its colours and images are not included.
* `user` — who made the change, described in [Identify who made a change](#identify-who-made-a-change).

`Theme:Updated` does not say which parts of the theme changed. The theme rules themselves are described in [Wallet pass themes](/configure/advanced-configuration/wallet/wallet-pass-themes.md).

</details>

<details>

<summary><strong>Redirect events</strong></summary>

`Redirect:Redirected` is sent when a user opens a minified URL (redirect).

</details>

### Identify who made a change

Configuration events name the person or API key behind a change, so that an audit trail can be kept outside The Wallet Crew. Pass events do not: they carry no actor at all.

Theme, automation and benefit template events (`Theme:*`, `Automation:*`, `BenefitTemplate:*`) carry the same `user` object, with three fields:

* `credentialType` — `User` for a signed-in back-office user, `ApiKey` for an API key, `Unknown` when the change happened outside a request and carries no identity.
* `id` — the user identifier at the identity provider for a `User`, `apiKey:{name}` for an `ApiKey`. It can be empty when `credentialType` is `Unknown`. It is the same identifier the configuration history records for that change.
* `name` — a display name, when the credential carries one. For an API key, it is the name the key was issued under.

None of these objects includes an email address: the [users API](/api-reference/advanced/users.md) resolves a user identifier to a person, with its email and name.

### FAQ

<details>

<summary><strong>Can I subscribe to all events?</strong></summary>

Yes. Use the `*` wildcard in the `events` array, for example `Customer:*`, to subscribe to all customer sub-events.

Use this carefully. You may receive more events than you need.

</details>

<details>

<summary><strong>Should my endpoint be public?</strong></summary>

Yes. The endpoint must be reachable from The Wallet Crew over HTTPS.

If you restrict inbound traffic, allowlist The Wallet Crew outgoing IPs and still validate the signature.

</details>

<details>

<summary><strong>Where do I find the exact payload schema for an event?</strong></summary>

Use the API reference. It is the source of truth for event names and payload shapes.

Start with [API reference](https://docs.thewalletcrew.io/api-reference/).

</details>

<details>

<summary><strong>Why does a webhook event not include the email of the person who made a change?</strong></summary>

Events identify a user by `id` only. Resolve that identifier with the [users API](/api-reference/advanced/users.md) to get the person behind it. A value of the form `apiKey:{name}` identifies an API key rather than a person, and `credentialType: Unknown` means no identity was attached to the change. Pass events identify nobody.

</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/developers-guides/integration-guides/webhooks.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.
