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
| Provider | Header | Algorithm |
|---|---|---|
generic | X-Efacts-Signature (or X-Webhook-Signature) | HMAC-SHA256 of the raw body, lowercase hex |
clover | X-Clover-Auth or X-Clover-Signature | HMAC-SHA256 of the raw body, hex |
square | X-Square-Signature or X-Square-Hmacsha256-Signature | HMAC-SHA256 of the raw body, base64 |
stripe | Stripe-Signature | Stripe'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.