API
Webhooks
receive real-time notifications for secret events.
Setup
configure a webhook URL when generating your API key:
curl -X POST https://noro.sh/api/v1/keys \
-H "Content-Type: application/json" \
-d '{"webhook":"https://example.com/webhook"}'the webhook URL must use HTTPS.
Events
secret.created
fired when a new secret is stored.
secret.viewed
fired when a secret is claimed (but views remain).
secret.expired
fired when the last view is consumed and the secret is deleted.
Payload
all webhook events include the same payload structure:
{
"event": "secret.created",
"timestamp": 1706000000000,
"data": {
"id": "abc123"
}
}Headers
webhook requests include a signature header for verification:
x-noro-signature: t=1706000000000,v1=5d41402abc4b...
the header contains a timestamp (t) and signature (v1). the signature is HMAC-SHA256 of timestamp.body.
Verification
verify the signature and check timestamp to prevent replay attacks:
const crypto = require("crypto");
function verify(body, header, secret) {
const [tPart, vPart] = header.split(",");
const timestamp = tPart.split("=")[1];
const signature = vPart.split("=")[1];
const age = Date.now() - parseInt(timestamp);
if (age > 300000) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(timestamp + "." + body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Delivery
webhooks are delivered via upstash qstash with automatic retries:
- •3 retry attempts on failure
- •exponential backoff between retries
- •10 second timeout per request
Best practices
- •always verify the signature
- •respond with 200 quickly, process async
- •handle duplicate deliveries idempotently
- •use a queue for processing