webhooks go both ways in heycapy. a bucket can receive items from other apps and scripts, and your reminders can go out to other apps.
into a bucket
every bucket has its own webhook. open bucket settings → advanced → configure webhook. you get a url and a key. if there’s no key yet, press rotate key to make one. rotating again makes a new key and the old one stops working.
send a POST with the key as a bearer token:
curl -X POST https://app.heycapy.xyz/api/webhook/12 \
-H "Authorization: Bearer <key>" \
-H "Content-Type: application/json" \
-d '{"title": "renew passport", "deadline": "2026-11-30"}'
the webhook screen in the app has this same command ready to copy, with your url and fields filled in.
| field | |
|---|---|
title | required, up to 500 characters |
deadline | optional. a date like 2026-11-30 makes an all-day item, a date and time like 2026-11-30T09:00 is read in your timezone, or add an offset like +05:30 |
status | optional. active, completed or on hold |
| your own fields | if the bucket has custom fields, send them by their key next to title |
a new item answers with 201 and its id. a wrong key gets 401, a missing title or a bad field gets 400 with what’s wrong, and more than 120 items a minute gets 429.
want a ping when something arrives? turn on notify on arrival in bucket settings → notifications → webhook items.
out to other apps
in tweaks → notifications → other apps, press [add app +]. give it a name, paste the webhook url and save. you can add up to 3. turn on on by default and every bucket whose channels follow your bucket defaults uses it, new ones included. for a bucket that picked its own channels, tick it in bucket settings → notifications → channels.
discord and slack
paste a discord or slack webhook link and heycapy picks it up. reminders arrive as normal messages, nothing else to set up.
your own server
anything else gets a POST with json like this:
{
"type": "item.reminder",
"timestamp": "2026-11-29T09:00:00.000Z",
"data": {
"title": "renew passport",
"message": "Reminder: \"renew passport\" is due tomorrow",
"item": {
"id": 41,
"title": "renew passport",
"status": "active",
"deadline": "2026-11-29T23:00:00.000Z",
"allDay": true,
"timezone": "Europe/Berlin",
"url": "https://app.heycapy.xyz/?bucket=12#item-41"
},
"bucket": { "id": 12, "name": "admin" }
}
}
type is item.reminder, item.overdue or item.arrived. a [send test] sends notification.
answer with any 2xx. if your server is down or answers with something else, heycapy tries again a couple of times over the next few minutes.
checking it’s really heycapy
each app gets its own secret, starting with whsec_. you’ll find it in the app’s form, where you can show, copy or replace it. every request is signed the standard webhooks way, with three headers:
webhook-id: the same on every retry, so you can skip repeatswebhook-timestamp: when it was sent, in secondswebhook-signature:v1,followed by the signature
the easiest way to check it is one of the standard webhooks libraries. doing it by hand: take the secret without whsec_, base64-decode it, and make an hmac-sha256 of webhook-id.webhook-timestamp.body with it. the base64 of that should match the part after v1,.