E-FACTSDocs
Webhook signatures

Webhook signatures

Every request to the ingest endpoint must be signed. E-FACTS verifies the signature against the raw request bytes before anything is processed. Unsigned or badly signed requests get 401 invalid_signature.

Signature by provider

ProviderHeaderAlgorithm
genericX-Efacts-Signature (or X-Webhook-Signature)HMAC-SHA256 of the raw body, lowercase hex
cloverX-Clover-Auth or X-Clover-SignatureHMAC-SHA256 of the raw body, hex
squareX-Square-Signature or X-Square-Hmacsha256-SignatureHMAC-SHA256 of the raw body, base64
stripeStripe-SignatureStripe's t=…,v1=… scheme over `${t}.${body}`, 300-second tolerance

For Square, Clover and Stripe, the POS signs requests for you: paste the secret it gives you into your E-FACTS onboarding. You only sign requests yourself on the generic adapter.

Signing a generic request

Node.js

import crypto from "node:crypto";

const body = JSON.stringify(receipt); // sign exactly what you send
const signature = crypto
  .createHmac("sha256", process.env.EFACTS_WEBHOOK_SECRET)
  .update(body)
  .digest("hex");

await fetch(`${process.env.EFACTS_BASE_URL}/api/v1/webhooks/ingest/generic`, {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-Efacts-Signature": signature },
  body,
});

Python

import hashlib, hmac, json, os, requests

body = json.dumps(receipt, separators=(",", ":")).encode()
signature = hmac.new(os.environ["EFACTS_WEBHOOK_SECRET"].encode(), body, hashlib.sha256).hexdigest()

requests.post(
    f'{os.environ["EFACTS_BASE_URL"]}/api/v1/webhooks/ingest/generic',
    data=body,  # send the same bytes you signed
    headers={"Content-Type": "application/json", "X-Efacts-Signature": signature},
    timeout=10,
)

Troubleshooting 401 invalid_signature

  • The body changed after signing. Serializing twice, pretty-printing, or curl -d (which strips newlines) all change the bytes. Sign and send the same buffer.
  • Wrong encoding. Generic and Clover expect hex; Square expects base64.
  • Wrong secret. Each environment has its own secret.
  • Stripe clock skew. Stripe signatures older than 300 seconds are rejected.

Rejected requests are kept for forensics, so support can look up the eventId from the 401 response.