VICIfast
All guides

Receiving VICIfast webhooks - endpoints, signatures and retries

How to get VICIfast events pushed to your server - adding an endpoint, checking the signature in Node.js, Python or PHP, answering fast, and catching up on anything missed.

VICIfastLast updated

A webhook is VICIfast calling you: when something happens on your account - numbers arriving, a call finishing, the wallet running low - we send an HTTPS POST with the details to an address you choose. This guide covers adding an endpoint, checking that a request really came from us, and handling retries and missed events.

Which events can I receive?

Nine, each named the same way in the request body's type:

  • numbers.arrived - numbers are yours: bought, delivered against a back-order, or assigned.
  • number.released - a number left your account.
  • number.renewal_failed - a monthly renewal could not be paid from the wallet.
  • number.routing_failed - a number could not be pointed at your server.
  • backorder.filled - every number a back-order asked for has arrived.
  • call.inbound.completed and call.outbound.completed - a call finished.
  • wallet.low - the balance dropped below a threshold you set.
  • wallet.topped_up - money was added to the wallet.

Each body is the same envelope: id, object: "event", type, livemode, created_at, and the event's own data.

How do I add an endpoint?

In the dashboard under API → Webhooks, or from code with a key that has webhooks:write:

curl -X POST https://vicifast.com/api/v1/webhook-endpoints \
  -H "Authorization: Bearer $VICIFAST_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/vicifast", "events": ["numbers.arrived", "call.inbound.completed"] }'

The answer includes the endpoint's signing secret. Keep it: the API shows it only when the endpoint is made and when you rotate it (the dashboard can show it again). The address must be HTTPS and on the public internet. An endpoint made with a test key receives only the sandbox's events.

No receiver yet? API → Webhooks has a hosted test inbox: an address of ours you can add as an endpoint, which shows every request it receives - headers, body, and whether the signature checks out.

How do I check a request is from VICIfast?

Every request carries an X-VICIfast-Signature header like t=1791000000,v1=5f2b.... v1 is an HMAC-SHA256, keyed with your endpoint's secret, of the timestamp, a full stop, and the raw request body. Recompute it, compare in constant time, and refuse a timestamp more than five minutes old so a captured request cannot be replayed.

Use the body exactly as it arrived - before any JSON parsing - or the signature will not match.

Node.js:

import crypto from 'node:crypto';

export function fromVicifast(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const want = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return typeof parts.v1 === 'string' && parts.v1.length === want.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(want));
}

Python:

import hashlib
import hmac
import time


def from_vicifast(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or abs(time.time() - int(t)) > 300:
        return False
    signed = t.encode() + b"." + raw_body
    want = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, v1)

PHP:

<?php
function from_vicifast(string $rawBody, string $header, string $secret): bool {
    $parts = [];
    foreach (explode(',', $header) as $p) {
        [$k, $v] = array_pad(explode('=', $p, 2), 2, '');
        $parts[$k] = $v;
    }
    $t = $parts['t'] ?? '';
    if (!ctype_digit($t) || abs(time() - (int) $t) > 300) {
        return false;
    }
    $want = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    return hash_equals($want, $parts['v1'] ?? '');
}

What should my endpoint answer?

Any 2xx, within ten seconds. Do the slow work after answering - put the event on a queue and acknowledge it.

Anything else is retried after 1, 5, 15, 60 and 240 minutes. A plain refusal - a 4xx other than 408 or 429 - is not retried, because sending the same request again will not change the answer. Samples sent with "send test" are tried once, and carry "test": true in their data.

How do I avoid handling an event twice?

Keep the id of each event you have handled and skip one you have seen. The id is the same on every retry and on a resend. X-VICIfast-Delivery identifies one delivery and its retries; a resend is a new delivery with a new value, so do not deduplicate on it.

How do I catch up on events I missed?

GET /api/v1/events lists the events of the last 30 days, newest first, each exactly as its webhook body - filter by type, from and to. POST /api/v1/events/{id}/resend sends one again, to every endpoint subscribed to it or to one you name. An event is recorded when at least one endpoint was subscribed to its type at the time.

Each endpoint's recent deliveries - what was sent, what came back, what is being retried - are at GET /api/v1/webhook-endpoints/{id}/deliveries, and in the dashboard under API → Webhooks.