PersonalBusinessEnterprise
    In a rush? Schedule an Appointment
    Fetching local time...
    Anytime CapitalforBusiness
    Pricing

    Get started

    • Overview
    • Quickstart
    • Test mode
    • Guides & recipes
    • Switch from Stripe
    • Import from Shopify
    • Import from WooCommerce
    • Import from Square
    • Import a CSV file
    • Stripe API mapping
    • Pricing
    • Support
    • FAQ

    Payments

    • Checkout & website
    • Guide: Next.js checkout
    • In person (QR)
    • Payment methods
    • Stablecoin checkout
    • Guide: take stablecoins
    • Fraud rules & 3D Secure
    • Dispute evidence
    • International customers
    • Guide: safe refunds
    • Bank transfers on invoices

    Billing

    • Subscription discounts
    • Guide: sell subscriptions
    • Usage billing
    • Guide: report usage

    Money movement

    • Payout options
    • Guide: reconcile payouts
    • Accounting sync
    • Revenue analytics

    Developers

    • Authentication
    • Errors & retries
    • Pagination
    • Idempotency
    • Rate limits
    • Webhooks
    • Verify webhooks
    • Node SDK (preview)
    • CLI (preview)
    • Postman collection

    AI

    • AI agents hub
    • Safe AI agents
    • Custom agents
    • Prompt library

    Integrations

    • Website builders
    • Shopify
    • Shopify orders by invoice
    • WooCommerce
    • Zapier
    • Slack, Discord and Teams

    Reference

    • Payments
    • Refunds
    • Disputes
    • Payment links
    • Invoices
    • Customers
    • Products
    • Subscriptions & plans
    • Discount codes
    • Meters & usage events
    • Payouts & balance
    • Webhook endpoints
    • Events
    • Test helpers
    • OpenAPI spec
    • Error codes
    • Changelog
    • Glossary

    For AI tools

    • llms-full.txt
    • MCP server
    • openapi.json
    Docs menu · Verify webhooks

    Get started

    • Overview
    • Quickstart
    • Test mode
    • Guides & recipes
    • Switch from Stripe
    • Import from Shopify
    • Import from WooCommerce
    • Import from Square
    • Import a CSV file
    • Stripe API mapping
    • Pricing
    • Support
    • FAQ

    Payments

    • Checkout & website
    • Guide: Next.js checkout
    • In person (QR)
    • Payment methods
    • Stablecoin checkout
    • Guide: take stablecoins
    • Fraud rules & 3D Secure
    • Dispute evidence
    • International customers
    • Guide: safe refunds
    • Bank transfers on invoices

    Billing

    • Subscription discounts
    • Guide: sell subscriptions
    • Usage billing
    • Guide: report usage

    Money movement

    • Payout options
    • Guide: reconcile payouts
    • Accounting sync
    • Revenue analytics

    Developers

    • Authentication
    • Errors & retries
    • Pagination
    • Idempotency
    • Rate limits
    • Webhooks
    • Verify webhooks
    • Node SDK (preview)
    • CLI (preview)
    • Postman collection

    AI

    • AI agents hub
    • Safe AI agents
    • Custom agents
    • Prompt library

    Integrations

    • Website builders
    • Shopify
    • Shopify orders by invoice
    • WooCommerce
    • Zapier
    • Slack, Discord and Teams

    Reference

    • Payments
    • Refunds
    • Disputes
    • Payment links
    • Invoices
    • Customers
    • Products
    • Subscriptions & plans
    • Discount codes
    • Meters & usage events
    • Payouts & balance
    • Webhook endpoints
    • Events
    • Test helpers
    • OpenAPI spec
    • Error codes
    • Changelog
    • Glossary

    For AI tools

    • llms-full.txt
    • MCP server
    • openapi.json
    DocsDevelopersVerify webhooks

    Verify webhook signatures in Node.js, Python and PHP

    Copy-paste webhook handlers that verify the Anytime-Signature header on the raw body: Express, Next.js, Flask, FastAPI and PHP, and how to test them locally.

    View as MarkdownOpen in Claude

    Every event we send carries an Anytime-Signature header such as t=1760630536,v1=5257a869…. v1 is the hex HMAC-SHA256 of the timestamp, a period and the raw request body, keyed with your endpoint's whole signing secret (whsec_…). Verify it before you trust the event, in constant time, and reject timestamps more than 5 minutes old so a captured request can't be replayed.

    While a signing secret is being rotated with an overlap, the header carries two v1 values, one per secret, newest first. Accept the event if any v1 matches: every handler below does, so you can deploy the new secret at any point in the overlap. Code that keeps only one v1 (for example by turning the header into a map) fails during a rotation.

    Use the raw body

    The signature covers the exact bytes we sent. Read the body before any JSON parser touches it: a body that was parsed and serialised again has different whitespace and key order, and won't verify. That's the usual reason a correct secret fails.

    • Keep the secret in a server-side environment variable such as ANYTIME_WEBHOOK_SECRET. Each endpoint has its own.
    • Answer any 2xx within 10 seconds, then do slow work; anything else is retried. See Webhooks.
    • Events can arrive more than once and out of order: store each id you've handled and skip repeats.

    Node.js (Express)

    express.raw keeps the body as a Buffer for this route; register it before any express.json() that would claim the same path.

    server.mjs
    // server.mjs — npm install express; run: node server.mjs
    import crypto from "node:crypto";
    import express from "express";
    
    const app = express();
    const secret = process.env.ANYTIME_WEBHOOK_SECRET; // whsec_…
    
    // Read the raw body: the signature covers the exact bytes we sent.
    app.post("/webhooks/anytime", express.raw({ type: "application/json" }), (req, res) => {
      const header = req.get("Anytime-Signature") ?? "";
      // t=<timestamp>, then one v1= per signing secret (two during a rotation).
      const pairs = header.split(",").map((part) => part.trim().split("="));
      const t = pairs.find(([key]) => key === "t")?.[1] ?? "";
      const signatures = pairs.filter(([key]) => key === "v1").map(([, value]) => value);
      const body = req.body.toString("utf8");
    
      const expected = Buffer.from(
        crypto.createHmac("sha256", secret).update(`${t}.${body}`).digest("hex"),
      );
      const signed = signatures.some(
        (signature) => signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), expected),
      );
      const fresh = Math.abs(Date.now() / 1000 - Number(t)) <= 300;
      if (!signed || !fresh) return res.status(400).send("Invalid signature");
    
      const event = JSON.parse(body);
      // The same event can arrive more than once: skip ids you've already handled.
      switch (event.type) {
        case "payment.succeeded":
          // Fulfil the order for event.data.id
          break;
        case "payment.refunded":
          // event.data.amountRefundedCents is the total refunded so far
          break;
      }
      res.sendStatus(200);
    });
    
    app.listen(3000);

    Next.js (App Router)

    app/api/webhooks/anytime/route.ts
    // app/api/webhooks/anytime/route.ts (Next.js App Router)
    import crypto from "node:crypto";
    
    export const runtime = "nodejs";
    
    export async function POST(request: Request) {
      const secret = process.env.ANYTIME_WEBHOOK_SECRET!; // whsec_…
      const body = await request.text(); // the raw body, exactly as sent
      const header = request.headers.get("Anytime-Signature") ?? "";
      // t=<timestamp>, then one v1= per signing secret (two while a secret is rotated).
      const pairs = header.split(",").map((part) => part.trim().split("="));
      const t = pairs.find(([key]) => key === "t")?.[1] ?? "";
      const signatures = pairs.filter(([key]) => key === "v1").map(([, value]) => value ?? "");
    
      const expected = Buffer.from(crypto.createHmac("sha256", secret).update(`${t}.${body}`).digest("hex"));
      const signed = signatures.some(
        (signature) => signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), expected),
      );
      const fresh = Math.abs(Date.now() / 1000 - Number(t)) <= 300;
      if (!signed || !fresh) return new Response("Invalid signature", { status: 400 });
    
      const event = JSON.parse(body);
      // Skip event.id if you've handled it before, then act on event.type.
      if (event.type === "payment.succeeded") {
        // Mark the order in event.data.metadata (or event.data.clientReference) paid.
      }
      return new Response(null, { status: 200 });
    }

    Python (Flask)

    app.py
    # app.py — pip install flask; run: flask --app app run
    import hashlib
    import hmac
    import json
    import os
    import time
    
    from flask import Flask, abort, request
    
    app = Flask(__name__)
    SECRET = os.environ["ANYTIME_WEBHOOK_SECRET"]  # whsec_…
    
    
    @app.post("/webhooks/anytime")
    def anytime_webhook():
        body = request.get_data()  # the raw bytes, exactly as sent
        header = request.headers.get("Anytime-Signature", "")
        # t=<timestamp>, then one v1= per signing secret (two while a secret is rotated).
        pairs = [part.strip().split("=", 1) for part in header.split(",") if "=" in part]
        timestamp = next((value for key, value in pairs if key == "t"), "")
        signatures = [value for key, value in pairs if key == "v1"]
        if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
            abort(400)
        expected = hmac.new(SECRET.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
        if not any(hmac.compare_digest(expected.encode(), signature.encode()) for signature in signatures):
            abort(400)
    
        event = json.loads(body)
        # Skip event["id"] if you've handled it before, then act on event["type"].
        if event["type"] == "payment.succeeded":
            pass  # Mark the order in event["data"]["metadata"] paid.
        return "", 200

    Python (FastAPI)

    main.py
    # main.py — pip install fastapi uvicorn; run: uvicorn main:app
    import hashlib
    import hmac
    import json
    import os
    import time
    
    from fastapi import FastAPI, HTTPException, Request
    
    app = FastAPI()
    SECRET = os.environ["ANYTIME_WEBHOOK_SECRET"]  # whsec_…
    
    
    @app.post("/webhooks/anytime")
    async def anytime_webhook(request: Request):
        body = await request.body()  # the raw bytes: don't take a parsed model here
        header = request.headers.get("Anytime-Signature", "")
        # t=<timestamp>, then one v1= per signing secret (two while a secret is rotated).
        pairs = [part.strip().split("=", 1) for part in header.split(",") if "=" in part]
        timestamp = next((value for key, value in pairs if key == "t"), "")
        signatures = [value for key, value in pairs if key == "v1"]
        if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
            raise HTTPException(status_code=400, detail="Invalid signature")
        expected = hmac.new(SECRET.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
        if not any(hmac.compare_digest(expected.encode(), signature.encode()) for signature in signatures):
            raise HTTPException(status_code=400, detail="Invalid signature")
    
        event = json.loads(body)
        # Skip event["id"] if you've handled it before, then act on event["type"].
        return {"received": True}

    PHP

    webhook.php
    <?php
    // webhook.php — plain PHP 7.4+, no framework.
    $secret = getenv('ANYTIME_WEBHOOK_SECRET'); // whsec_…
    $payload = file_get_contents('php://input'); // the raw body, exactly as sent
    $header = $_SERVER['HTTP_ANYTIME_SIGNATURE'] ?? '';
    
    // t=<timestamp>, then one v1= per signing secret (two while a secret is rotated).
    $timestamp = '';
    $signatures = [];
    foreach (explode(',', $header) as $part) {
        [$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
        if ($key === 't') {
            $timestamp = $value;
        } elseif ($key === 'v1') {
            $signatures[] = $value;
        }
    }
    $expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
    $signed = false;
    foreach ($signatures as $signature) {
        $signed = $signed || hash_equals($expected, $signature);
    }
    
    if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300 || !$signed) {
        http_response_code(400);
        exit('Invalid signature');
    }
    
    $event = json_decode($payload, true);
    // Skip $event['id'] if you've handled it before, then act on $event['type'].
    if ($event['type'] === 'payment.succeeded') {
        // Mark the order in $event['data']['metadata'] paid.
    }
    http_response_code(200);

    In Laravel, read the raw body with $request->getContent() and the header with $request->header('Anytime-Signature'), and exclude the route from CSRF protection; the check is the same.

    Node SDK

    With the Node SDK (preview), AnytimeCapital.webhooks.constructEvent(rawBody, header, secret) verifies and parses in one call.

    Test your handler

    • Send test event in Settings → Developers sends a signed webhook.test event to a deployed endpoint, whatever events it subscribes to.
    • On your own machine, sign a body yourself with your secret and post it to your local server:
    Sign and send a test event
    SECRET="whsec_…"   # your endpoint's signing secret
    BODY='{"id":"evt_local_1","type":"webhook.test","created":"2026-10-24T12:00:00.000Z","data":{}}'
    T=$(date +%s)
    SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
    
    curl -X POST http://localhost:3000/webhooks/anytime \
      -H "Content-Type: application/json" \
      -H "Anytime-Signature: t=$T,v1=$SIG" \
      -d "$BODY"

    The URL above is the Express handler's. For the others use http://localhost:3000/api/webhooks/anytime (Next.js), http://localhost:5000/webhooks/anytime (Flask) or http://localhost:8000/webhooks/anytime (uvicorn), or wherever your PHP script is served. Change one character of the body after signing and your handler should answer 400.

    PreviousWebhooksNext Node SDK (preview)

    Anytime Capital is a fintech company, not a bank. Payment processing, banking and payout services are provided by Anytime Capital's licensed partners. Rates shown are for US card payments; international cards, currency conversion and disputes may carry additional fees shown in your dashboard before you accept a payment.

    On this page

    • Node.js (Express)
    • Next.js (App Router)
    • Python (Flask)
    • Python (FastAPI)
    • PHP
    • Node SDK
    • Test your handler
    Anytime Capital
    (470) 748-2530
    App Store

    Business

    • Business Checking
    • Business Debit Cards
    • Payments
    • Ecommerce Payments
    • Invoicing
    • Developer Docs

    Solutions

    • CPAs & Accountants
    • Financial Advisors
    • Boat & Auto Dealerships
    • Dental & Medical Practices
    • Creators & Coaches
    • All solutions

    Crypto

    • Crypto Wallet
    • Tether Wallet
    • Buy Digital Assets
    • Explore Markets
    • Security & Licenses
    • Compare Platforms
    • Bitcoin ATM Alternative

    Company

    • Our Story
    • Schedule Appointment
    • Careers
    • Media & Press
    • Investor Relations
    • FAQ

    Branches

    • Atlanta, Georgia
    • Miami, Florida
    • Houston, Texas
    • All branches

    Legal

    • Terms & Conditions
    • Privacy Policy
    • Your Privacy Choices
    • Fraud Notice

    © 2026 Anytime Capital LLC. Anytime Capital is a fintech company, not a bank.

    The Anytime Capital Mastercard® Prepaid Card is issued by Pathward®, N.A., Member FDIC. Mastercard® is a registered trademark of Mastercard International Incorporated. All other trademarks and service marks belong to their respective owners.

    Mastercard