Agent skill

GlobalStack Webhooks Skill

Receive and verify GlobalStack webhooks (stablecoin and fiat money-movement API, delivered via Svix). Use when setting up GlobalStack webhook handlers, debugging svix-signature verification, or handling events like onramp.success, offramp.failed, conversion.success, transfer.success, deposit.confirmed, and kyc.approved.

Install this skill

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


GlobalStack is a stablecoin and fiat money-movement API: onramps, offramps, conversions, transfers, deposits and KYC. It delivers webhooks through Svix, which implements Standard Webhooks. Every delivery is an HTTP POST with a JSON body and three signature headers: svix-id, svix-timestamp, and svix-signature. The body is a flat envelope: the event name is the top-level event_type, and the resource (the same body as the matching REST resource, plus event-only fields) is in event_object.

When to Use This Skill

  • How do I receive GlobalStack webhooks?
  • How do I verify GlobalStack webhook signatures (svix-signature)?
  • How do I handle onramp.success, offramp.returned or conversion.failed events?
  • How do I react to deposit.held, deposit.confirmed or kyc.approved?
  • Why is my GlobalStack webhook signature verification failing, or rejecting Hookdeck retries?

Verification (core)

Signed content is {svix-id}.{svix-timestamp}.{raw body}. Svix signs it with HMAC-SHA256 using the base64-decoded secret (the part after whsec_), base64-encodes the result, and sends one or more space-separated v1,<signature> entries. Use the official svix library, which compares in constant time. Read the svix-* headers, and when svix-id is absent fall back to the webhook-* headers (webhook-id, webhook-timestamp, webhook-signature), as Hookdeck does.

import { Webhook } from 'svix'; // svix 2.x is ESM-only

const prefix = req.headers['svix-id'] ? 'svix' : 'webhook';
const id = req.headers[`${prefix}-id`];
const timestamp = req.headers[`${prefix}-timestamp`];
const signature = req.headers[`${prefix}-signature`];
// rawBody: the raw Buffer/string, never re-serialized JSON
new Webhook(process.env.GLOBALSTACK_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': id, 'svix-timestamp': timestamp, 'svix-signature': signature,
  'webhook-id': id, 'webhook-timestamp': timestamp, 'webhook-signature': signature,
}); // throws on a bad signature or a timestamp outside the window
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["GLOBALSTACK_WEBHOOK_SECRET"])  # "whsec_..."
wh.verify(raw_body, {"svix-id": id, "svix-timestamp": ts, "svix-signature": sig,
                     "webhook-id": id, "webhook-timestamp": ts, "webhook-signature": sig})

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

The signing secret is per endpoint, starts with whsec_, and is shown in plaintext once: in the response to POST /v1/notifications/webhooks (and again, new, when you rotate it) as signing_secret. Keep the full whsec_... value in GLOBALSTACK_WEBHOOK_SECRET. Handlers fail closed: with the variable unset they return 500 instead of accepting anything.

Hookdeck retries and replays hit the 5-minute window

The svix library always enforces a fixed 5-minute timestamp window, and it can't be configured. Hookdeck retries and replays resend the original svix-timestamp, so the library rejects any retry or replay that happens more than 5 minutes after the original delivery. If you verify behind Hookdeck (or replay old events), use the manual verifier in references/verification.md, where the window can be dropped or widened. Deduplicate on event_id (or svix-id) instead of relying on the timestamp.

Common Event Types

27 events are documented (the list may not be exhaustive). The name is event_type in the JSON body, and event_category is the family you subscribed to. The examples handle all of these:

EventTriggered When
onramp.awaiting_depositAn onramp is created and waiting for the payer's deposit
onramp.funds_receivedThe deposit arrived (also offramp.funds_received, conversion.funds_received)
onramp.processingThe conversion or payout is in flight (also offramp.processing, conversion.processing)
onramp.successDone, destination credited. Terminal (also offramp.success, conversion.success)
onramp.failedCould not complete. Terminal (also offramp.failed, conversion.failed)
onramp.returnedFunds were sent back to their origin. Terminal (also offramp.returned, conversion.returned)
offramp.createdAn offramp is created and accepted (also conversion.created)
transfer.processingA payout has been submitted
transfer.successA payout completed. Terminal
transfer.failedA payout failed, was cancelled, or was reversed. Terminal
deposit.receivedFunds detected but not yet final
deposit.heldCredited but withheld pending review
deposit.confirmedSettled and spendable. Terminal
deposit.failedA deposit could not be credited. Terminal
kyc.approvedA customer verification passed
kyc.rejectedA customer verification was declined

See references/overview.md for the envelope, payload extras and delivery rules.

Environment Variables

# Signing secret of the webhook endpoint (Svix format, starts with whsec_)
GLOBALSTACK_WEBHOOK_SECRET=whsec_xxxxx

Local Development

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

Create the endpoint with POST /v1/notifications/webhooks using the tunnel URL, store the signing_secret, then activate it with PATCH /v1/notifications/webhooks/{id} {"active": true}. New endpoints are created inactive and receive nothing until then. See references/setup.md.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →