PlunkPlunk
Recipes

Sync unsubscribes to your database

Mirror Plunk's subscription state into your own user table using a workflow + webhook

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

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.

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.

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.

Enable the workflow.

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:

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