# Kit unsubscribe notifications on your phone

> Get a low-priority push notification when someone unsubscribes from your Kit newsletter, and a louder one when they report your email as spam.

- Company: Kit (https://www.justpush.io/recipes/kit)
- Event: `subscriber.unsubscribed` (Subscriber unsubscribed)
- Tags: Email marketing, Newsletters
- Install: https://studio.justpush.io/recipes/kit/unsubscribe
- Web page: https://www.justpush.io/recipes/kit/unsubscribe

## Setup

1. Click Install in Studio and sign in. The recipe is added to your account and you get a personal webhook URL.
2. In Kit, open app.kit.com/webhooks and create a webhook endpoint.
3. Paste your webhook URL (shown in Studio after install) as the URL.
4. Subscribe it to Subscriber unsubscribed only and save.
5. Subscribe the endpoint to `subscriber.unsubscribed`, and add `subscriber.complained` if you also want to hear about spam complaints.
6. You can also create the endpoint through the API:
7. ```bash
8. curl -X POST https://api.kit.com/v4/webhook_endpoints \
9. -H "X-Kit-Api-Key: <your-api-key>" \
10. -H "Content-Type: application/json" \
11. -d '{ "url": "your webhook URL (shown in Studio after install)", "events": ["subscriber.unsubscribed", "subscriber.complained"] }'
12. ```
13. Kit signs each delivery (`X-Kit-Signature`), but JustPush doesn't check that signature yet, so keep your endpoint URL private. Webhooks need a Kit plan with API access.

## Code

Studio calls `handleRequest(request)` with the incoming webhook (`{ method, headers, body, query, raw }`) and sends the message object it returns. Returning `null` sends nothing.

```js
// Kit → subscriber.unsubscribed
// Fires when a subscriber unsubscribes in Kit. Also handles subscriber.complained
// (they reported your email as spam), which gets a louder push.
// Docs: https://developers.kit.com/webhooks/event-types
//
// Kit's webhook endpoints send { delivery_id, events: [ … ] } with up to 100 events of
// one type per delivery. Legacy webhooks (/v4/webhooks) send a bare { subscriber: { … } }.

// Event types this recipe handles, and the legacy subscriber state that matches each.
const TYPES = {
    "subscriber.unsubscribed": "cancelled",
    "subscriber.complained": "complained",
}

// Kit sends plain strings; treat blanks as missing.
function text(value) {
    return typeof value === "string" && value.trim() ? value.trim() : null
}

// "Ada (ada@example.com)", or whichever half is there.
function describe(subscriber) {
    const name = text(subscriber?.first_name)
    const email = text(subscriber?.email_address)
    return name && email ? `${name} (${email})` : name || email || "Someone"
}

function handleRequest(request) {
    const body = request.body && typeof request.body === "object" ? request.body : {}
    const has = (key) => Object.prototype.hasOwnProperty.call(TYPES, key)

    let type = null
    let subscribers = []
    if (Array.isArray(body.events)) {
        // Webhook endpoints: every event in a delivery has the same type.
        const events = body.events.filter((e) => has(e?.type) && e.data?.subscriber)
        type = events[0]?.type ?? null
        subscribers = events.filter((e) => e.type === type).map((e) => e.data.subscriber)
    } else if (body.subscriber && typeof body.subscriber === "object") {
        // Legacy webhooks carry no event name; the subscriber's state tells us what happened.
        type = Object.keys(TYPES).find((key) => TYPES[key] === body.subscriber.state) ?? null
        subscribers = type ? [body.subscriber] : []
    }

    // Guard: nothing this recipe handles, e.g. a new subscriber or a tag event.
    if (!type || !subscribers.length) {
        console.log(`Ignoring Kit "${body.events?.[0]?.type ?? body.subscriber?.state ?? "unknown"}" delivery`)
        return null
    }

    const spam = type === "subscriber.complained"
    const who =
        subscribers.length === 1
            ? describe(subscribers[0])
            : subscribers.slice(0, 3).map((s) => text(s.email_address) ?? "someone").join(", ") +
              (subscribers.length > 3 ? ` and ${subscribers.length - 3} more` : "")

    return {
        title: spam
            ? subscribers.length > 1 ? `⚠️ ${subscribers.length} spam complaints` : "⚠️ Spam complaint"
            : subscribers.length > 1 ? `👋 ${subscribers.length} unsubscribes` : "👋 Unsubscribed",
        message: spam ? `${who} reported your email as spam` : `${who} unsubscribed`,
        topic: "Kit",
        priority: spam ? 0 : -1, // Normal for a spam complaint (hurts deliverability), Low for routine churn
    }
}
```

## Kit subscriber.unsubscribed webhook payload (sample)

```json
{
  "events": [
    {
      "id": "1f0b6c2d-3e4a-4b5c-8d9e-0a1b2c3d4e5f",
      "data": {
        "subscriber": {
          "id": 987654,
          "state": "cancelled",
          "fields": [],
          "created_at": "2026-06-12T08:15:00Z",
          "first_name": "Ada",
          "email_address": "ada@example.com"
        }
      },
      "type": "subscriber.unsubscribed",
      "created": "2026-09-29T10:02:44Z"
    }
  ],
  "delivery_id": 123457
}
```

## FAQ

### Does this work on iPhone and Android?

Yes. Install the JustPush app from the App Store or Google Play and sign in. Every phone signed in to your account gets the notification.

### Do I need to write code?

No. Install the recipe in Studio, paste your webhook URL into Kit and you are done. The code is there if you want to change the text, the sound or the buttons.

### Can I change what the notification says?

Yes. After install the recipe's code is yours. Edit it in Studio and test it against the sample payload before you save.

### What does it cost?

JustPush is free for 30 days. After that it's $19.99 a year, or $39.99 once. There is no extra charge for recipes.
