Skip to content

Webhooks

When a buyer completes payment on an ExtendPay session and it reaches Complete, Billtrust can POST a signed JSON notification to your webhook URL.

Webhooks are optional. Without both a URL and a signing secret, Billtrust does not call your system.

Configuration

Successful webhook delivery requires both:

Setting Description
Webhook URL Absolute https URL that accepts Billtrust POSTs
Signing secret Secret starting with whsec_…, used to sign ExtendPay-Signature

These values are not set through the ExtendPay session API. A supplier administrator configures them in Billtrust payments configuration (or an internal Billtrust resource does so on your behalf). After the secret is generated, store the value securely and use it when verifying signatures. Regenerating the secret replaces it immediately—update your verifier at the same time.

Delivery

POST {your-webhookUrl}
Content-Type: application/json; charset=utf-8
ExtendPay-Signature: t=1710000000,v1=abc123...

Billtrust:

  • Sends POST only
  • Does not follow redirects
  • Waits up to 10 seconds
  • Treats any 2xx as success
  • Retries on non-2xx, timeouts, and connection errors

Payload

{
  "apiVersion": "v1",
  "type": "extendpay.session.completed",
  "createdAt": "2026-08-11T15:12:04Z",
  "data": {
    "extendPaySessionId": "c0ffee00-0000-4000-8000-000000000001",
    "status": "Complete",
    "sourceType": "supplierPortal",
    "accountNumber": "ACCT-100",
    "user": {
      "email": "buyer@example.com",
      "firstName": "Ada",
      "lastName": "Lovelace"
    },
    "onCompleteUrl": "https://supplier.example/return",
    "createdAt": "2026-08-11T15:00:00Z",
    "updatedAt": "2026-08-11T15:12:04Z",
    "paymentReference": "pay-ref-1",
    "transactions": [
      {
        "status": "Complete",
        "totalAmount": 175.0,
        "currencyCode": "USD",
        "transactionReference": "txn-1",
        "paymentDate": "2026-08-11T15:11:30Z",
        "scheduled": false,
        "paymentAccountDisplayName": "Visa ••4242",
        "invoices": {
          "items": [
            {
              "invoiceNumber": "INV-100",
              "totalDue": 100.0,
              "dueDate": "2026-09-15",
              "billDate": "2026-08-01",
              "amountApplied": 100.0,
              "appliedDiscount": 0.0,
              "isShortPay": false,
              "isOverPay": false
            }
          ]
        },
        "statements": {
          "items": [
            {
              "totalDue": 75.0,
              "dueDate": "2026-09-20",
              "billDate": "2026-08-05",
              "amountApplied": 75.0,
              "appliedDiscount": 5.0,
              "isShortPay": true,
              "isOverPay": false
            }
          ]
        }
      }
    ]
  }
}
Field Description
apiVersion v1
type extendpay.session.completed
createdAt When the notification was created (UTC)
data Session body — same fields as Get session

Verify the signature against the raw body exactly as received. Do not re-serialize JSON before verifying.

Verify the signature

ExtendPay-Signature: t=<unixSeconds>,v1=<hexDigest>
Part Meaning
t Unix timestamp (seconds)
v1 Lowercase hex HMAC-SHA256 digest

Steps

  1. Read the raw request body.
  2. Parse t and v1 from ExtendPay-Signature.
  3. Reject if |now - t| is greater than 300 seconds (5 minutes).
  4. Build {t}.{rawBody}.
  5. Compute HMAC-SHA256 with your full signing secret (including whsec_).
  6. Hex-encode in lowercase and compare to v1 with a constant-time check.
  7. Only then parse and process the JSON.
Example (Node.js)
const crypto = require('crypto');

function verifyExtendPaySignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((p) => {
      const [k, v] = p.trim().split('=');
      return [k, v];
    })
  );

  const timestamp = parts.t;
  const expected = parts.v1;
  if (!timestamp || !expected) {
    return false;
  }

  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (Number.isNaN(age) || age > toleranceSeconds) {
    return false;
  }

  const signedPayload = `${timestamp}.${rawBody}`;
  const digest = crypto
    .createHmac('sha256', secret)
    .update(signedPayload, 'utf8')
    .digest('hex');

  const a = Buffer.from(digest, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  if (a.length !== b.length) {
    return false;
  }
  return crypto.timingSafeEqual(a, b);
}
Example (C#)
using System.Security.Cryptography;
using System.Text;

static bool VerifyExtendPaySignature(
    string rawBody,
    string signatureHeader,
    string secret,
    int toleranceSeconds = 300)
{
    long? t = null;
    string v1 = null;

    foreach (var part in signatureHeader.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
    {
        var idx = part.IndexOf('=');
        if (idx <= 0) continue;
        var key = part[..idx];
        var value = part[(idx + 1)..];
        if (key == "t" && long.TryParse(value, out var ts)) t = ts;
        if (key == "v1") v1 = value;
    }

    if (t is null || string.IsNullOrEmpty(v1)) return false;

    var age = Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t.Value);
    if (age > toleranceSeconds) return false;

    var signedPayload = $"{t.Value}.{rawBody}";
    var keyBytes = Encoding.UTF8.GetBytes(secret);
    var payloadBytes = Encoding.UTF8.GetBytes(signedPayload);
    var hash = HMACSHA256.HashData(keyBytes, payloadBytes);
    var digest = Convert.ToHexString(hash).ToLowerInvariant();

    var a = Encoding.UTF8.GetBytes(digest);
    var b = Encoding.UTF8.GetBytes(v1);
    return a.Length == b.Length && CryptographicOperations.FixedTimeEquals(a, b);
}

Your response

Your response Billtrust behavior
2xx Success
Non-2xx, timeout, or connection error Retry
Slow handler (> 10s) Treated as failure; may retry

Verify the signature, acknowledge quickly with 2xx, and process heavy work asynchronously.

Retries

Failed deliveries retry about every two minutes, up to 10 attempts. After that, fetch the session with Get session. There is no replay API.

Security checklist

  • Use HTTPS only
  • Verify ExtendPay-Signature on every request
  • Enforce the 5-minute timestamp window
  • Store the signing secret securely
  • Update your verifier immediately after rotating the secret
  • Deduplicate on data.extendPaySessionId