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-Signatureheader? - Why does my SpherePay webhook signature verification fail?
- How do I handle
customer.approvedortransfer.succeededevents? - 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
standardwebhooksorsvixlibraries.
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_ 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
POSTper 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 usesequenceto discard stale updates.
Common Event Types
The event name is the type field of the JSON body.
| Event | Description |
|---|---|
customer.created | POST /v2/customer succeeded |
customer.pending | All required criteria resolved for a verification profile |
customer.approved | Verification profile approved |
customer.rejected | Verification profile rejected |
transfer.created | POST /v2/transfer succeeded |
transfer.fundsReceived | Funds detected from the customer |
transfer.succeeded | Terminal success: funds delivered |
transfer.failed | Terminal 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
| Header | Description |
|---|---|
Sphere-Signature | Bare lowercase hex HMAC-SHA256 of {timestamp}.{raw body} |
Sphere-Timestamp | Unix seconds when this attempt was sent (signed) |
Sphere-Delivery-Id | The event delivery this attempt belongs to |
Sphere-Delivery-Attempt | Attempt counter, starting at 1 |
Sphere-Delivery-Type | original 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
- references/overview.md - SpherePay webhook concepts and events
- references/setup.md - Registering an endpoint and getting the secret
- references/verification.md - Signature verification details and the published test vector