Agent skill

SpherePay Webhooks Skill

Receive and verify SpherePay webhooks. Use when setting up SpherePay v2 webhook handlers, debugging Sphere-Signature / Sphere-Timestamp verification, or handling customer.approved, customer.rejected, transfer.succeeded, and transfer.failed events from SpherePay's stablecoin on- and off-ramp API.

Install this skill

npx skills add hookdeck/webhook-skills --skill spherepay-webhooks


When to Use This Skill

  • How do I receive SpherePay webhooks?
  • How do I verify the Sphere-Signature header?
  • Why does my SpherePay webhook signature verification fail?
  • How do I handle customer.approved or transfer.succeeded events?
  • How do I handle SpherePay webhooks when there are no automatic retries?

Essential Code (USE THIS)

SpherePay (v2 webhooks) sends a bare lowercase hex HMAC-SHA256 digest in Sphere-Signature. The signed message is the Sphere-Timestamp header value, a literal ., then the raw request body bytes. The key is the endpoint's signing secret (whsec_...) used whole as a plain UTF-8 string, including the whsec_ prefix.

This is not Standard Webhooks: nothing is base64-decoded and the digest is hex. Do not use the standardwebhooks or svix libraries.

Paste the whole secret, including the whsec_ prefix. SpherePay uses it as a plain HMAC key.

Verification (Node.js)

const crypto = require('crypto');

// rawBody: Buffer of the exact request bytes (express.raw, not express.json)
function verifySpherePay(rawBody, headers, secret) {
  const signature = headers['sphere-signature'];
  const timestamp = headers['sphere-timestamp'];
  if (!secret || !signature || !timestamp) return false;
  if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false; // validate before decoding

  // No timestamp window by default (see "Timestamp window" below)
  const signed = Buffer.concat([Buffer.from(timestamp, 'latin1'), Buffer.from('.'), rawBody]);
  const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(signed).digest();
  const received = Buffer.from(signature.toLowerCase(), 'hex');
  return expected.length === received.length && crypto.timingSafeEqual(expected, received);
}

Verification (Python)

import hashlib, hmac, re

def verify_spherepay(raw_body: bytes, headers, secret: str) -> bool:
    signature = headers.get("sphere-signature")
    timestamp = headers.get("sphere-timestamp")
    if not secret or not signature or not timestamp:
        return False
    if not re.fullmatch(r"[0-9a-fA-F]{64}", signature):  # validate before decoding
        return False
    signed = b"".join([timestamp.encode("latin-1"), b".", raw_body])
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).digest()
    return hmac.compare_digest(expected, bytes.fromhex(signature.lower()))

Respond 401 when a header is missing or empty, the signature is malformed, or it does not match. Parse the JSON only after the check passes.

Timestamp window: SpherePay's docs suggest rejecting a Sphere-Timestamp older than 5 minutes. The examples do not by default, because Hookdeck retries and replays resend the original headers and timestamp, so a fixed window would reject legitimate redeliveries. To opt in, set SPHEREPAY_WEBHOOK_TOLERANCE_SECONDS to a positive number of seconds (for example 300); unset or 0 is off. Sphere-Timestamp is Unix seconds.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Delivery Behaviour

  • One POST per event per endpoint; any 2xx within 15 seconds acknowledges it.
  • No automatic retries. A non-2xx or timeout marks the delivery failed; it can only be replayed manually through the Events API. Failures never disable the endpoint. Acknowledge fast and do heavy work asynchronously.
  • At-least-once and unordered: dedupe on the body's id (stable across replays) and use sequence to discard stale updates.

Common Event Types

The event name is the type field of the JSON body.

EventDescription
customer.createdPOST /v2/customer succeeded
customer.pendingAll required criteria resolved for a verification profile
customer.approvedVerification profile approved
customer.rejectedVerification profile rejected
transfer.createdPOST /v2/transfer succeeded
transfer.fundsReceivedFunds detected from the customer
transfer.succeededTerminal success: funds delivered
transfer.failedTerminal failure

Others: transfer.pendingFunding, transfer.pendingReview, transfer.processing, transfer.returned, transfer.pendingRefundInformation, transfer.refunded, transfer.canceled, transfer.expired, transfer.failedPrecondition, transfer.unexpectedError. See references/overview.md.

For the full event reference, see SpherePay's event catalog.

Important Headers

HeaderDescription
Sphere-SignatureBare lowercase hex HMAC-SHA256 of {timestamp}.{raw body}
Sphere-TimestampUnix seconds when this attempt was sent (signed)
Sphere-Delivery-IdThe event delivery this attempt belongs to
Sphere-Delivery-AttemptAttempt counter, starting at 1
Sphere-Delivery-Typeoriginal or replay

Environment Variables

SPHEREPAY_WEBHOOK_SECRET=whsec_your_endpoint_signing_secret   # returned once when the endpoint is created
SPHEREPAY_WEBHOOK_TOLERANCE_SECONDS=0   # optional; a positive value opts in to the replay window

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 spherepay --path /webhooks/spherepay

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →