Maven sends an HTTP POST to your webhook URL when a payment session completes or fails. This is the recommended way to get payment results back into your system.
Voice and chat widget both use the same webhook. One webhook URL per project, one handler in your code — it processes both channels. Use the caller field to distinguish: it’s the customer’s phone number for voice, and null for chat.
Webhooks are sent when a session reaches a terminal payment status:
Status
When
payment-success
Charge was authorized and captured immediately (default)
payment-authorized
Authorize.net auth-only charge succeeded — funds held but not captured. You settle the auth yourself in your gateway dashboard or via the gateway’s API.
payment-failed
Charge declined or gateway error
Webhooks are not sent for expired, cancelled, or abandoned sessions — poll the GET session endpoint for those.
Your webhook URL is public, so anyone could POST a forged event to it. Maven signs every webhook with an HMAC so you can confirm it genuinely came from us and wasn’t altered in transit. Verification is optional but strongly recommended for production.
Signing is backward compatible — it only adds a header. If you don’t verify it, your existing handler keeps working unchanged. Adopt verification whenever you’re ready.
Each app has its own signing secret (format whsec_…). Find it in the Dashboard under App → Settings → Webhook → Signing secret, where you can reveal, copy, and rotate it.
Rotating generates a new secret and invalidates the old one immediately. Update your server with the new value before (or right after) rotating, or signatures will start failing.
Read the raw request body — the exact bytes, before any JSON parsing/re-serialization.
Parse t and v1 from the Maven-Signature header.
Compute HMAC_SHA256(secret, "{t}." + raw_body).
Constant-time compare it against v1.
Reject if t is older than your tolerance (e.g. 5 minutes) to block replays.
Python (FastAPI)
Node (Express)
import hmac, hashlib, time, jsonfrom fastapi import FastAPI, Request, HTTPExceptionWEBHOOK_SECRET = "whsec_..." # from the dashboardTOLERANCE_SECONDS = 300def verify(raw_body: bytes, signature_header: str) -> bool: try: parts = dict(p.split("=", 1) for p in signature_header.split(",")) t, sig = parts["t"], parts["v1"] except (ValueError, KeyError): return False if abs(time.time() - int(t)) > TOLERANCE_SECONDS: return False # too old — possible replay signed = f"{t}.".encode() + raw_body expected = hmac.new(WEBHOOK_SECRET.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, sig)app = FastAPI()@app.post("/webhooks/maven")async def maven_webhook(request: Request): raw = await request.body() # RAW bytes, not request.json() if not verify(raw, request.headers.get("Maven-Signature", "")): raise HTTPException(status_code=401, detail="invalid signature") payload = json.loads(raw) # ... trusted return {"status": "ok"}
const crypto = require("crypto");const WEBHOOK_SECRET = process.env.MAVEN_WEBHOOK_SECRET;const TOLERANCE_SECONDS = 300;function verify(rawBody, signatureHeader) { const parts = Object.fromEntries( (signatureHeader || "").split(",").map((p) => p.split("=")), ); const { t, v1 } = parts; if (!t || !v1) return false; if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCE_SECONDS) return false; const expected = crypto .createHmac("sha256", WEBHOOK_SECRET) .update(`${t}.`) .update(rawBody) // Buffer of the raw body .digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));}// Capture the raw body so the bytes match what we signed:app.post( "/webhooks/maven", express.raw({ type: "application/json" }), (req, res) => { if (!verify(req.body, req.get("Maven-Signature"))) { return res.status(401).send("invalid signature"); } const payload = JSON.parse(req.body.toString()); res.sendStatus(200); },);
Sign the raw bytes, not the parsed JSON.{"a":1,"b":2} and {"b":2,"a":1} are equal objects but different bytes, so re-serializing before hashing produces a mismatching signature. Always HMAC the request body exactly as received, and use a constant-time comparison (hmac.compare_digest / crypto.timingSafeEqual).
Use the Test button next to your webhook URL in the dashboard — it sends a signed sample payload so you can validate your verification code end-to-end.
All webhook payloads share the same top-level fields. The processor object varies by gateway and mode — see Processor Fields by Gateway for the full specs.
Funds are held but not captured. Use processor.transaction_id to settle the charge later in your Authorize.net dashboard or via your own priorAuthCaptureTransaction API call. See Authorize.net Setup → Capture Mode.
Return a 200 response immediately and process the webhook asynchronously. Maven uses a 5-second delivery timeout; if your endpoint is slower, the request is treated as failed and retried.
Use idempotent handlers
Use the session_id to deduplicate — check if you’ve already processed this session before acting on it.
Verify the signature
Confirm the event came from Maven by verifying the Maven-Signature header — see Verifying Webhook Signatures. As an additional check, you can also confirm the session_id belongs to your organization via the API:
GET /v1/sessions/{session_id}
Handle failures gracefully
If your endpoint returns a 5xx or times out, Maven retries up to 3 times with exponential backoff (1s, 2s, 4s). A 4xx response is treated as a rejection and is not retried. If all attempts fail the webhook is dropped, so for critical flows also poll the session status as a fallback.