# Post deadline reminders to Slack

> Turn compliance events into a message your team sees.

Source: https://app.getoatmilk.com/docs/tutorials/compliance-alerts-in-slack

Oatmilk keeps a checklist of filing and payment deadlines, such as GST/HST returns, and emails administrators before they're due. This tutorial also posts them to a Slack channel, using a webhook and a Slack incoming webhook. It takes about thirty lines.

## 1. Create a Slack incoming webhook

In Slack, create an app with **Incoming Webhooks** turned on, add it to your channel, and copy its address. Keep it as `SLACK_WEBHOOK_URL`.

## 2. Subscribe to compliance events

```bash
curl https://app.getoatmilk.com/api/v1/accounting/webhooks.endpoints.create \
  -H "Authorization: Bearer $OATMILK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "url": "https://alerts.example.com/oatmilk",
  "description": "Slack deadline alerts",
  "events": [
    "compliance.item.due_soon",
    "compliance.item.overdue",
    "compliance.item.completed"
  ]
}'
```

A reminder event says what's due, when, and in how many days:

```http
POST /webhooks/oatmilk HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: Oatmilk-Webhooks/1.0
Oatmilk-Signature: t=1790000000,v1=e274e987bac8a111368a110d00f29d7591e8eeaf501a97e042fed5b7033a8954
Oatmilk-Event-Id: 4b4cdc02-0d1e-4f2a-8b3c-4d5e6f7a8b9c
Oatmilk-Event-Type: compliance.item.due_soon
Oatmilk-Delivery-Id: 3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f
Oatmilk-Delivery-Attempt: 1

{
  "id": "4b4cdc02-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
  "type": "compliance.item.due_soon",
  "created": 1790000000,
  "organizationId": "org_synthetic",
  "subject": {
    "type": "compliance_item",
    "id": "7f0f6c1e-1c1f-4b5e-9c8d-2f5e8e3c1a10"
  },
  "data": {
    "ruleKey": "gst_hst_return",
    "title": "GST/HST return",
    "dueDate": "2026-10-31",
    "daysUntil": 14,
    "digestKey": "gst_hst_return:2026-10-31"
  }
}
```

## 3. Post the message

```js title="alerts.js"
export async function POST(request) {
  const rawBody = await request.text();
  if (!verifyOatmilkSignature(rawBody, request.headers.get("Oatmilk-Signature") ?? "", process.env.OATMILK_WEBHOOK_SECRET)) return new Response(null, { status: 400 });
  const event = JSON.parse(rawBody);
  const text = message(event);
  if (text) await fetch(process.env.SLACK_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
  return new Response(null, { status: 200 });
}

function message({ type, data }) {
  if (type === "compliance.item.due_soon") return `:calendar: *${data.title}* is due ${data.dueDate} (in ${data.daysUntil} days).`;
  if (type === "compliance.item.overdue") return `:warning: *${data.title}* was due ${data.dueDate}.`;
  if (type === "compliance.item.completed") return `:white_check_mark: *${data.title}* is done.`;
  return null;
}
```

This is a Next.js route handler; any framework that gives you the raw body works the same way. `verifyOatmilkSignature` is the function from [Verify signatures](https://app.getoatmilk.com/docs/webhooks/signatures.md).

## 4. Try it

```bash
curl https://app.getoatmilk.com/api/v1/accounting/webhooks.test \
  -H "Authorization: Bearer $OATMILK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "endpointId": "5d4c3b2a-1f0e-4d9c-8b7a-6e5f4d3c2b1a",
  "eventType": "compliance.item.due_soon"
}'
```

Your channel gets the message within a few seconds.
