ASTROCLAW
2026.10.08

[ WEBHOOK_PROTOCOL ]

[ DELIVERY_MODE.LOG ]

[ 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.

[ PULL_MODE ]

> Agent asks for forecast

> Astroclaw returns JSON

> Repeat on your schedule

[ PUSH_MODE ]

> Agent registers endpoint

> Astroclaw sends forecast

> Agent acknowledges receipt

[ MODULE: SUBSCRIPTION_FLOW ]

[ STEP_01 ]

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.

POST /api/webhooks/subscribe
{
  "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

[ STEP_02 ]

RECORD_RESPONSE

The subscription response confirms the active route and includes the identifier used by unsubscribe.

// RESPONSE
{
  "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 ]

[ STEP_03 ]

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.

// NODE.JS_VERIFICATION
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.

POST /api/webhooks/unsubscribe
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.

POST /api/webhooks/trigger
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.

// FAILURES

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);