# Zift Webhooks

Zift (a payments platform / gateway) delivers notifications — billing and
processing events — to an HTTPS endpoint you register with Zift support. Each
delivery is a plain HTTPS POST with a JSON body. There is no signature,
no HMAC, and no auth header on the delivery. Instead, your endpoint proves it
received the notification by echoing the `notificationId` back in the response
body — that response is the acknowledgement, and skipping it triggers retries.

## When to Use This Skill

* How do I receive Zift webhook notifications?
* How do I acknowledge a Zift webhook (the `{"notificationId": ...}` response)?
* Why is there no `X-Zift-Signature` / HMAC header to verify?
* How do I secure a Zift webhook endpoint with no signature?
* How do I handle Zift `billing.*` and `processing.*` events (chargeback, return, NOC)?
* Why does Zift keep retrying / marking my webhook `Failed`?

## Verification & Acknowledgement (core)

There is NO per-message signature/HMAC header on Zift notifications. Do not
look for `X-Zift-Signature`, `webhook-signature`, or any Standard Webhooks
header — none exists, and Zift sends no Basic/token auth on delivery either.
Authenticity relies on the transport and endpoint secrecy:

1. HTTPS only — publish the endpoint over TLS so the body can't be read or
  tampered with in transit.
2. Endpoint-URL secrecy — treat the path as a shared secret; use a long,
  unguessable path.
3. IP allowlisting (recommended) — restrict inbound to Zift's egress ranges
  at your load balancer / firewall (confirm ranges with Zift support).

Because there is no body signature, ordinary JSON parsing is fine — the raw
bytes are not security-critical (unlike HMAC providers).

The critical part is the acknowledgement. To ACK a delivery your endpoint
MUST return a JSON body echoing the received `notificationId` (Zift accepts an
int or a string). Anything else — `200 OK`, an empty body, `{"received":true}` —
is not an acknowledgement and triggers Zift's retry schedule.

```javascript
// Zift acknowledgement: echo the received notificationId back, preserving its
// type (Zift accepts int or string). This response body IS the ACK — returning
// "OK" or {} instead causes Zift to retry (+5m, +15m, +60m, +24h, then Failed).
function ackBody(payload) {
  return { notificationId: payload.notificationId };
}

// Dispatch on the eventCode prefix: "billing.*" or "processing.*".
function eventCategory(eventCode) {
  if (typeof eventCode !== 'string') return 'unknown';
  const category = eventCode.split('.')[0];
  return category === 'billing' || category === 'processing' ? category : 'unknown';
}

```

> For complete handlers with route wiring, event dispatch, and tests, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zift-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zift-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zift-webhooks/examples/fastapi/)

## Payload Structure

Every notification shares the same envelope:

```json
{
  "notificationId": 272638,
  "eventCode": "billing.subscription-created",
  "eventDate": 1753670400000,
  "dataType": "subscription",
  "data": { }
}

```

| Field | Type | Meaning |
| --- | --- | --- |
| `notificationId` | int or string | Unique delivery id — echo this back to ACK |
| `eventCode` | string | Dotted event name, e.g. `billing.subscription-created` |
| `eventDate` | number | Event time in epoch milliseconds |
| `dataType` | string | Describes the shape of `data` (e.g. `subscription`) |
| `data` | object | Event-specific payload |

## Common Event Types

Events are grouped by the `eventCode` prefix. Only `billing.subscription-created`
is documented verbatim; the others follow Zift's `category.entity-action` pattern
from the setup trigger names — confirm the exact literal strings with Zift
support at onboarding and dispatch on the `billing` / `processing` prefix.

| `eventCode` | Category | Triggered when |
| --- | --- | --- |
| `billing.subscription-created` | billing | A recurring subscription is created |
| `billing.payment-option-created` | billing | A payment option (method) is added |
| `billing.allocation-created` | billing | A billing allocation is created |
| `billing.payment-processed` | billing | A scheduled billing payment is processed |
| `processing.chargeback` | processing | A chargeback is received |
| `processing.return` | processing | An ACH/eCheck return occurs |
| `processing.reversal` | processing | A transaction is reversed |
| `processing.noc` | processing | An ACH Notice of Change (NOC) is received |

See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zift-webhooks/references/overview.md) for the setup trigger names
(`subscription~create`, `payment-option~create`, `chargeback`, `NOC`, …).

## Retry Behaviour

If your endpoint does not acknowledge with the `notificationId`, Zift retries at
+5 min, +15 min, +60 min, +24 h, then marks the notification `Failed`
with no further redelivery. Make your handler idempotent — a retry may deliver a
notification you already processed.

## Environment Variables

Zift sends no secret on delivery, so no signing secret is required. The only
optional config is your own comma-separated IP allowlist (enforced by your
infra, shown here for documentation):

```bash
# Optional — Zift egress IP ranges to allow (confirm with Zift support).
# Enforcement belongs at your load balancer / firewall; this is informational.
ZIFT_ALLOWED_IPS=

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 zift --path /webhooks/zift

```

## Setup

Zift webhooks are configured out-of-band: email Zift support your HTTPS
endpoint URL and the events you want. Webhooks are set at the
integrator/reseller level, not per-merchant. See
[references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zift-webhooks/references/setup.md).

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zift-webhooks/references/overview.md) - Notifications, payload envelope, event codes and trigger names
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zift-webhooks/references/setup.md) - Registering the endpoint with Zift support
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zift-webhooks/references/verification.md) - Why there's no signature, the acknowledgement contract, securing the endpoint