# Sync unsubscribes to your database (/recipes/sync-unsubscribes)

Every flip of a contact's `subscribed` state — manual edits, the hosted unsubscribe page, bounces, complaints — fires a `contact.unsubscribed` event. Wire a workflow with a `WEBHOOK` step to forward that to your backend.

## Setup

import {Step, Steps} from 'fumadocs-ui/components/steps';

<Steps>
  <Step>
    ### Build the receiving endpoint

    A public HTTPS endpoint that verifies a shared secret and updates the user row. Webhook requests time out after 10 seconds, so do the work async if it's slow.

    ```ts
    app.post('/plunk/unsubscribes', async (req, res) => {
      if (req.header('authorization') !== `Bearer ${process.env.PLUNK_WEBHOOK_SECRET}`) {
        return res.status(401).end();
      }

      const { contact, event } = req.body;
      await db.user.update({
        where: { email: contact.email },
        data: {
          emailSubscribed: false,
          emailUnsubscribedReason: event.reason ?? 'user_action',
        },
      });

      res.status(204).end();
    });
    ```

    `event.reason` is `"bounce"` or `"complaint"` for automatic unsubscribes, and absent for manual / self-service ones.
  </Step>

  <Step>
    ### Create the workflow

    **Workflows → New workflow**:

    * **Trigger**: `EVENT` on `contact.unsubscribed`
    * Add a `WEBHOOK` step:
      * **URL**: `https://api.example.com/plunk/unsubscribes`
      * **Headers**: `{ "Authorization": "Bearer your-shared-secret" }`
      * Leave the body blank to get the [default payload](/guides/webhooks#webhook-payload).

    Enable the workflow.
  </Step>
</Steps>

## Mirroring resubscribes

Build a second workflow with the same shape, triggered by `contact.subscribed`. Keep it separate from the unsubscribe flow — two short workflows are easier to monitor than one branched one.

## The reverse direction

If your product is the source of truth (a user toggles their email preference in your settings UI), call `PATCH /contacts/:id` from your backend:

```bash
curl -X PATCH https://next-api.useplunk.com/contacts/cnt_abc \
  -H "Authorization: Bearer sk_your_secret_key" \
  -d '{"subscribed": false}'
```

That flip also fires `contact.unsubscribed`, meaning your own webhook will round-trip back into your handler. That's usually harmless because the update is idempotent — but be aware of it.

## What's next

<Cards>
  <Card title="Webhooks" href="/guides/webhooks">
    Webhook step reference, payload shape, and safety.
  </Card>

  <Card title="Unsubscribe pages" href="/guides/unsubscribe-pages">
    The hosted pages and template URL variables.
  </Card>
</Cards>
