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