Skip to main content

Webhooks

Valcr sends signed HTTP POST requests to your endpoint when key account events occur. Webhooks are critical for catching quota exhaustion and billing failures without polling.


Configuring a webhook

In the Console under Webhooks, provide:

  • An HTTPS endpoint URL
  • The events you want to receive

Or via API:

curl -X POST "https://api.valcr.site/api/v1/console/webhooks" \
-H "Authorization: Bearer {session_token}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/valcr",
"events": ["quota.exhausted", "invoice.failed", "key.rotated"]
}'

Response:

{
"webhook_id": "wh_01HXXXXXXXXXXXXXXXX",
"url": "https://your-server.com/webhooks/valcr",
"events": ["quota.exhausted", "invoice.failed", "key.rotated"],
"signing_secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"created_at": "2025-01-14T09:00:00Z"
}

Store the signing_secret — you'll use it to verify incoming events.


Event types

EventWhen it fires
key.createdA new API key was created
key.rotatedAn API key was rotated (old key now invalid)
key.revokedAn API key was permanently revoked
quota.warningAccount quota reached 80%
quota.exhaustedQuota fully consumed — auto-billing triggered
invoice.paidPayment successfully processed
invoice.failedPayment attempt failed

Payload structure

All events share the same envelope:

{
"id": "evt_01HXXXXXXXXXXXXXXXX",
"type": "quota.exhausted",
"account_id": "acc_01HXXXXXXXXXXXXXXXX",
"created_at": "2025-01-14T09:22:01Z",
"data": {
// event-specific payload — see below
}
}

quota.exhausted

{
"id": "evt_01HXXXXXXXXXXXXXXXX",
"type": "quota.exhausted",
"data": {
"calls_used": 10000,
"period_start": "2025-01-01T00:00:00Z",
"period_end": "2025-01-31T23:59:59Z",
"charge_amount": 2900,
"charge_currency": "USD",
"paystack_ref": "PAY_xxxxxxxxxxxxxxxx"
}
}

key.rotated

{
"id": "evt_01HXXXXXXXXXXXXXXXX",
"type": "key.rotated",
"data": {
"key_id": "key_01HXXXXXXXXXXXXXXXX",
"key_name": "Production underwriting",
"rotated_at": "2025-01-14T09:00:00Z"
}
}

invoice.paid

{
"id": "evt_01HXXXXXXXXXXXXXXXX",
"type": "invoice.paid",
"data": {
"invoice_id": "inv_01HXXXXXXXXXXXXXXXX",
"amount": 2900,
"currency": "USD",
"paid_at": "2025-01-14T09:22:01Z",
"paystack_ref": "PAY_xxxxxxxxxxxxxxxx"
}
}

Verifying signatures

Every webhook request includes an X-Valcr-Signature header containing an HMAC-SHA256 signature. Always verify this before processing the event.

Python

import hmac, hashlib

def verify_valcr_webhook(payload_bytes: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload_bytes,
hashlib.sha256,
).hexdigest()
received = signature_header.split("sha256=")[-1]
return hmac.compare_digest(expected, received)

# In your Flask/FastAPI handler:
@app.post("/webhooks/valcr")
async def handle_webhook(request: Request):
body = await request.body()
sig = request.headers.get("X-Valcr-Signature", "")
secret = os.environ["VALCR_WEBHOOK_SECRET"]

if not verify_valcr_webhook(body, sig, secret):
raise HTTPException(status_code=400, detail="Invalid signature")

event = json.loads(body)
await process_event(event)
return {"ok": True}

Node.js

import crypto from 'crypto'

function verifyValcrWebhook(
payload: Buffer,
signatureHeader: string,
secret: string,
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
const received = signatureHeader.replace('sha256=', '')
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(received),
)
}

// Express handler
app.post('/webhooks/valcr', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.headers['x-valcr-signature'] as string
const secret = process.env.VALCR_WEBHOOK_SECRET!

if (!verifyValcrWebhook(req.body, sig, secret)) {
return res.status(400).json({ error: 'Invalid signature' })
}

const event = JSON.parse(req.body.toString())
processEvent(event)
res.json({ ok: true })
})
Always verify signatures

Never process a webhook without verifying the signature. Unverified webhooks are a common attack vector for triggering billing or access changes.


Retry policy

If your endpoint returns a non-2xx status or times out (> 30 seconds), Valcr retries:

AttemptDelay
1Immediate
230 seconds
35 minutes
430 minutes
52 hours

After 5 failed attempts, the event is marked as dead and no further retries occur. The Console shows delivery status for each event.


Testing your endpoint

Use the Test button in the Console, or:

curl -X POST "https://api.valcr.site/api/v1/console/webhooks/test" \
-H "Authorization: Bearer {session_token}"

This sends a synthetic quota.warning event to your configured URL.


Responding correctly

Your endpoint must:

  • Return 2xx within 30 seconds
  • Accept Content-Type: application/json
  • Not depend on the order of events (events can arrive out of order)

Respond with a simple acknowledgement:

{ "ok": true }

Long processing should be queued asynchronously — acknowledge immediately, process in the background.