Webhooks
Receive real-time notifications when events happen in your PaperStack account.
Webhooks
Webhooks allow you to receive real-time HTTP notifications when events occur in your PaperStack account — like a key being created, revoked, or a billing status change.
How It Works
- You register a webhook endpoint (an HTTPS URL) in your Dashboard
- PaperStack sends a
POSTrequest to your endpoint when an event fires - Your server responds with
200 OKwithin 10 seconds to acknowledge receipt - If delivery fails, we retry up to 3 times with exponential backoff (5s, 25s, 125s)
Supported Events
| Event | Description |
|---|---|
key.created | A new API key was created |
key.revoked | An existing API key was revoked |
key.expired | A key reached its expiration date |
usage.quota_80 | You've used 80% of your monthly quota |
usage.quota_100 | You've hit your monthly limit |
billing.plan_upgraded | Your plan was upgraded |
billing.plan_downgraded | Your plan was downgraded |
billing.payment_failed | A payment attempt failed |
billing.subscription_canceled | Your subscription ended |
Registering a Webhook
- Go to your Dashboard
- Click Add Webhook
- Enter your HTTPS endpoint URL
- Select the events you want to receive
- Save — PaperStack generates a unique HMAC signing secret
Verifying Delivery (Signature)
Every webhook request includes an X-PaperStack-Signature header:
X-PaperStack-Signature: sha256=abc123...Verify the signature using your webhook's secret:
import crypto from 'crypto';
function verifySignature(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}import hmac
import hashlib
def verify_signature(payload, signature, secret):
expected = 'sha256=' + hmac.new(
secret.encode(),
json.dumps(payload, separators=(',', ':')).encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
)
func verifySignature(payload interface{}, signature string, secret string) bool {
data, _ := json.Marshal(payload)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(data)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}Payload Format
Every webhook request sends the following JSON body:
{
"id": "unique-delivery-id",
"event": "key.created",
"timestamp": "2026-05-28T12:00:00.000Z",
"data": {
"keyId": "key_xxx",
"name": "Production Key"
}
}Delivery Headers
| Header | Description |
|---|---|
X-PaperStack-Signature | HMAC-SHA256 signature of the payload |
X-PaperStack-Event | The event type (e.g. key.created) |
X-PaperStack-Delivery | Unique delivery ID for idempotency |
Best Practices
- Respond with
200 OKas quickly as possible — process asynchronously - Verify the signature before trusting the payload
- Use idempotency keys — the
X-PaperStack-Deliveryheader is unique per attempt - Store the secret securely — it's shown only once when you create the webhook
- Rotate secrets periodically via the dashboard
Delivery Log
Your Dashboard shows a delivery log with:
- Delivery status (success/failure)
- HTTP status code
- Response body (first 1KB)
- Duration
- Attempt number
Failed deliveries are retried 3 times. If a webhook fails 10+ consecutive times, it is automatically marked as failing.
Was this page helpful?