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

Whenever a contact's `subscribed` flag turns off — whether from a manual edit, the hosted unsubscribe page, a bounce, or a spam complaint — Bitelio emits `contact.unsubscribed`. A workflow listening for that event can relay it straight to your backend through a `WEBHOOK` step.

## Setup

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

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

    Expose a public HTTPS route that checks a shared secret before touching the user record. Keep the handler fast — webhook deliveries are cut off after 10 seconds, so push anything slow onto a queue.

    ```ts
    app.post('/bitelio/unsubscribes', async (req, res) => {
      if (req.header('authorization') !== `Bearer ${process.env.BITELIO_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();
    });
    ```

    When the unsubscribe was automatic, `event.reason` carries `"bounce"` or `"complaint"`; for manual or self-service opt-outs the field simply isn't there.
  </Step>

  <Step>
    ### Create the workflow

    Under **Workflows → New workflow**:

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

    Turn the workflow on.
  </Step>
</Steps>

## Mirroring resubscribes

For the opposite direction of the flag, clone the same structure into a second workflow triggered on `contact.subscribed`. Don't merge the two into one branched flow — a pair of small, single-purpose workflows is much simpler to keep an eye on.

## The reverse direction

When the authoritative copy lives on your side — say a user flips their email preference inside your own settings page — push the change to Bitelio with `PATCH /contacts/:id` from your backend:

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

Be aware this update emits `contact.unsubscribed` too, so the webhook you built above will echo the change right back to your handler. Since the write is idempotent the loop is normally harmless — just don't be surprised by it.

## What's next

<Cards>
  <Card title="Webhooks" href="/guides/webhooks">
    Full webhook step reference: payload structure and safety notes.
  </Card>

  <Card title="Unsubscribe pages" href="/guides/unsubscribe-pages">
    Hosted preference pages and the URL variables available in templates.
  </Card>
</Cards>
