[ WEBHOOK_PROTOCOL ]
[ ADVANCED_FEATURE_ENABLED ]
Webhook Delivery
Push-based horoscope delivery for agents that can receive signed HTTP POST requests. Use this when your agent has a public HTTPS endpoint and should receive forecasts automatically.
> Agent asks for forecast
> Astroclaw returns JSON
> Repeat on your schedule
> Agent registers endpoint
> Astroclaw sends forecast
> Agent acknowledges receipt
[ MODULE: SUBSCRIPTION_FLOW ]
CREATE_SUBSCRIPTION
Register one webhook URL and one zodiac sign. Astroclaw immediately POSTs a webhook.ping event to the URL; reply with any 2xx to confirm you want deliveries.
{
"webhook_url": "https://your-agent.com/webhook",
"sign": "cancer",
"secret": "your_optional_hmac_secret"
}
webhook_url Public HTTPS endpoint, required
sign Zodiac sign to receive, required
secret HMAC secret, optional but recommended
RECORD_RESPONSE
The subscription response confirms the active route and includes the identifier used by unsubscribe.
{
"success": true,
"subscription_id": "sub_Xk3v9QeR2mT7aB1c",
"management_token": "whk_…",
"sign": "cancer",
"webhook_url": "https://your-agent.com/webhook",
"signed": true,
"created_at": "2026-03-26T00:00:00Z"
}
Save subscription_id and management_token. The token is shown once and is required to test or remove the subscription.
[ MODULE: DELIVERY_CONTRACT ]
[ PAYLOAD_FORMAT ]
Astroclaw sends this JSON body once per new forecast. Test deliveries carry "test": true.
{
"event": "daily.forecast",
"test": false,
"sign": "cancer",
"date": "2026-03-26",
"forecast": "Your optical sensors are clear today...",
"url": "https://www.astroclaw.xyz/forecasts/2026-03-26/cancer/",
"json_url": "https://www.astroclaw.xyz/api/forecasts/2026-03-26/cancer.json",
"timestamp": "2026-03-26T02:00:04Z"
}
[ HTTP_HEADERS ]
Content-Type
application/json
User-Agent
Astroclaw-Webhook/2.0
X-Astroclaw-Event
daily.forecast or webhook.ping
X-Astroclaw-Delivery
Unique delivery ID
X-Astroclaw-Subscription
Your subscription ID
X-Astroclaw-Signature
sha256=HMAC of the raw body, when a secret exists
[ MODULE: SECURITY ]
VERIFY_HMAC_SIGNATURE
If a subscription includes a secret, Astroclaw signs the raw request body with HMAC-SHA256. Verify against the bytes you received, before parsing JSON.
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return signature?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
const signature = req.headers['x-astroclaw-signature'];
const isValid = verifyWebhook(rawBody, signature, process.env.WEBHOOK_SECRET);
> Use HTTPS for webhook URLs
> Verify HMAC before side effects
> Store secrets in environment variables
> Return 2xx after successful receipt
[ UNSUBSCRIBE ]
Stop delivery with the saved subscription ID and token.
Authorization: Bearer whk_…
{
"subscription_id": "sub_1234567890_abc123"
}
[ TEST_TRIGGER ]
Send a test delivery to your own endpoint now. Optional date picks a past forecast.
Authorization: Bearer whk_…
{
"subscription_id": "sub_Xk3v9QeR2mT7aB1c"
}
[ SCHEDULE ]
Deliveries run once a day at about 02:00 UTC, after the new forecast is published. Each forecast is delivered at most once.
One attempt per day, 10s timeout, no redirects. Paused after 7 failed days in a row.
[ MODULE: EXAMPLE_HANDLER ]
[ EXPRESS_JS_RECEIVER ] ▼
const express = require('express');
const crypto = require('crypto');
const app = express();
// Keep the raw body: the signature covers the exact bytes sent.
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const secret = process.env.WEBHOOK_SECRET;
if (secret) {
const signature = req.headers['x-astroclaw-signature'] || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
if (signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
}
const payload = JSON.parse(req.body);
if (payload.event === 'webhook.ping') {
return res.status(200).json({ ok: true });
}
console.log(`Received horoscope for ${payload.sign} on ${payload.date}`);
console.log(payload.forecast);
res.status(200).json({ received: true });
});
app.listen(3000);