Webhooks
XipFax doesn’t offer a way to register an endpoint yet. Until it does, no event is posted. Each event goes to every webhook endpoint of your account when a fax you sent is delivered or fails for good, and when a fax sent to your number arrives, as webhooks describes each: {"id", "type", "api_version", "created_at", "data"}, the fax in data as GET /faxes gives it. Each delivery is signed per Standard Webhooks with the endpoint’s own secret, shown once, when the endpoint is registered: verify webhook-signature before you trust the body. XipFax posts only to https at a public address, and follows no redirect. Answer a 2xx within 15 seconds. After a 408, a 429, a 5xx or no answer, XipFax tries again over about three days, later where Retry-After asks for longer, up to a day; after a redirect or another 4xx, it stops. An event may come more than once, or after a later one: take each id once.
The events
fax.delivered: A fax was delivered.fax.failed: A fax failed.fax.received: A fax arrived.
Check a delivery
Before you read a delivery, check its webhook-signature against its body exactly as it came, before anything parses it. Each sample below also refuses a delivery more than five minutes from your own clock, and takes any of the signatures a delivery holds.
import { createHmac, timingSafeEqual } from 'node:crypto';
/** Whether a delivery is XipFax's: its body as it came, its headers and your endpoint's secret. */
function isFromXipFax(body, headers, secret) {
const timestamp = headers['webhook-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace('whsec_', ''), 'base64');
const signed = `${headers['webhook-id']}.${timestamp}.${body}`;
const expected = createHmac('sha256', key).update(signed).digest();
return headers['webhook-signature'].split(' ').some((signature) => {
const given = Buffer.from(signature.replace('v1,', ''), 'base64');
return (
signature.startsWith('v1,') &&
given.length === expected.length &&
timingSafeEqual(given, expected)
);
});
}This browser didn’t let us copy it. Select the code to copy it.
import base64, hashlib, hmac, time
def is_from_xipfax(body: bytes, headers, secret: str) -> bool:
"""Whether a delivery is XipFax's: its body as it came, its headers and your endpoint's secret."""
timestamp = headers["webhook-timestamp"]
if abs(time.time() - int(timestamp)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f'{headers["webhook-id"]}.{timestamp}.'.encode() + body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
return any(
signature.startswith("v1,") and hmac.compare_digest(signature[3:], expected)
for signature in headers["webhook-signature"].split(" ")
)This browser didn’t let us copy it. Select the code to copy it.