EAuth docs

EAuth

Webhooks

Your application is told when something happens, instead of asking.

Events

Type When
user.signed_in A session was established for your application. Not on refresh.
consent.granted Somebody agreed to the scopes you asked for.
consent.revoked Somebody disconnected your application from their account page.
session.revoked A session was ended, by the user or by us.
user.password_changed Relevant if you cache anything derived from the old one.

An endpoint subscribed to no events receives all of them.

What a delivery looks like

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: EAuth-Webhooks/1.0
Elchi-Event-Id: 01a06c8a-…
Elchi-Signature: t=1788000000,v1=5f3a…

{
  "id": "01a06c8a-…",
  "type": "consent.revoked",
  "created_at": "2026-09-04T10:12:00Z",
  "client_id": "eauth_cnf_…",
  "data": { "user_id": "…" }
}

Verifying the signature

The conventions are Stripe's, so an existing verifier will work with the header name changed. The signature is HMAC-SHA256 over timestamp + "." + body using your endpoint's secret.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, header, rawBody, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (age > toleranceSeconds) throw new Error("stale");

  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.`).update(rawBody).digest("hex");
  if (!timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) {
    throw new Error("bad signature");
  }
}

Verify against the raw request body, before any JSON parsing. A parser that reorders keys or normalises whitespace changes the bytes and the signature stops matching.

The timestamp is part of what is signed, so a captured delivery cannot be replayed later even though nothing about it changed. Reject anything older than five minutes.

Retries

A delivery that does not get a 2xx response is retried eight times over roughly a day: after 30 seconds, then 2, 10 and 30 minutes, then 2, 6 and 12 hours. A receiver down for ten minutes loses nothing.

Every attempt carries the same Elchi-Event-Id. If you see one twice, you already handled it; discard the second without parsing.

After 25 consecutive failures the endpoint is disabled and shown as such in the console, where it can be re-enabled once fixed. Individual deliveries can be replayed from there too.

Rules for the URL

https only, a public hostname, no credentials in it. Private and reserved addresses are refused at registration and again at connection time, so a hostname that later resolves to something internal is still refused. Redirects are not followed.

Respond fast and do the work afterwards. The request times out after ten seconds, and a slow handler is retried like a failed one.

3 min read · Elchi Studios, Zug · Terms