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.returnedorconversion.failedevents? - How do I react to
deposit.held,deposit.confirmedorkyc.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_. 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:
| Event | Triggered When |
|---|---|
onramp.awaiting_deposit | An onramp is created and waiting for the payer's deposit |
onramp.funds_received | The deposit arrived (also offramp.funds_received, conversion.funds_) |
onramp.processing | The conversion or payout is in flight (also offramp.processing, conversion.processing) |
onramp.success | Done, destination credited. Terminal (also offramp.success, conversion.success) |
onramp.failed | Could not complete. Terminal (also offramp.failed, conversion.failed) |
onramp.returned | Funds were sent back to their origin. Terminal (also offramp.returned, conversion.returned) |
offramp.created | An offramp is created and accepted (also conversion.created) |
transfer.processing | A payout has been submitted |
transfer.success | A payout completed. Terminal |
transfer.failed | A payout failed, was cancelled, or was reversed. Terminal |
deposit.received | Funds detected but not yet final |
deposit.held | Credited but withheld pending review |
deposit.confirmed | Settled and spendable. Terminal |
deposit.failed | A deposit could not be credited. Terminal |
kyc.approved | A customer verification passed |
kyc.rejected | A 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
- references/overview.md - GlobalStack webhook concepts, events, envelope, delivery rules
- references/setup.md - Create and activate an endpoint, get the secret, rotate, replay
- references/verification.md - Svix verification details, header fallback, manual verifier, timestamp window