Tutorials
Post deadline reminders to Slack
Turn compliance events into a message your team sees.
10 minutes · Beginner
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
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:
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
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.
4. Try it
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.