Agent skill
Recurrente Webhooks Skill
Receive and verify Recurrente webhooks (Guatemalan payments platform: cards, bank transfer, crypto, balance, cash, subscriptions, transfers; delivered via Svix). Use when setting up Recurrente webhook handlers, debugging svix-signature verification, or handling events like intent.succeeded, subscription.past_due, verification.rejected, and refund.create.
Install this skill
npx skills add hookdeck/webhook-skills --skill recurrente-webhooks
Recurrente 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 event name is the top-level event_type field. In the unified intent.* payload, the top-level type is the payment method (payment, bank_transfer, crypto, balance, cash), not the event.
Recurrente's docs are in Spanish. Endpoints receive every event (no per-endpoint filter is documented), so ignore the ones you don't need and return 200 for them.
When to Use This Skill
- How do I receive Recurrente webhooks?
- How do I verify Recurrente webhook signatures (
svix-signature)? - How do I handle
intent.succeededorintent.failedevents? - How do I react to
subscription.past_dueorsubscription.cancel? - Why is my Recurrente 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.RECURRENTE_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["RECURRENTE_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/.
Each endpoint has its own secret, shaped like whsec_ (that is the docs' example, not a real secret). It is returned once, as signingSecret, by POST /api/webhook_, and can be read at any time in the Svix dashboard reachable from Configuración > Desarrolladores y API > Webhooks. Keep the full whsec_... value in RECURRENTE_. Handlers fail closed: with the variable unset they return 500 instead of accepting anything. Missing headers or a bad signature return 400.
Hookdeck retries and replays hit the 5-minute window
The svix and standardwebhooks libraries always enforce 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. Recurrente's docs call the timestamp check optional ("unos minutos", no number). 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 svix-id instead of relying on the timestamp.
Common Event Types
The name is event_type in the JSON body. The examples handle these:
| Event | Triggered When |
|---|---|
intent.succeeded | A charge succeeded (any payment method); funds are in your balance |
intent.pending | A charge started or is pending (bank transfer, crypto) |
intent.failed | A charge failed |
intent.canceled | An intent was canceled (one "l") |
intent.paid | Balance payments only, sent to the paying account |
subscription.create | A subscription is created (also .past_due, .pause, .unpause, .reactivate, .update, .item_added, .item_removed, .invoice_item_added, .cancel) |
setup_intent.succeeded | A trial subscription starts, or a card is tokenized without a charge |
setup_intent.cancelled | Tokenizing a card failed (two "l"s) |
refund.create | A refund was processed |
dispute.create | A chargeback was opened (also dispute.update) |
verification.completed | A connected account's verification was accepted |
verification.rejected | Documents were rejected; read document_errors |
tax_ | FEL tax invoicing finished activating |
account_ | A child account connected to a platform |
transfer.sent | A p2p transfer was sent (also transfer.received) |
withdrawal.create | A bank withdrawal was created (also withdrawal.update) |
Accounts created after the unified-format change receive only intent.* for payments; older accounts also keep the legacy per-method events (payment_intent.*, bank_transfer_intent.*, balance_intent.*, crypto_intent.*, cash_intent.succeeded, automated_). See references/overview.md for the full list, payload shape, and retries.
Environment Variables
# Signing secret of the webhook endpoint (Svix format, starts with whsec_)
RECURRENTE_WEBHOOK_SECRET=whsec_xxxxx
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 recurrente --path /webhooks/recurrente
Register the tunnel URL as an endpoint under Configuración > Desarrolladores y API > Webhooks, then pay a Sandbox checkout with card 4242 4242 4242 4242 to receive intent.succeeded.
Reference Materials
- references/overview.md - Recurrente webhook concepts, events, payload shape, retries
- references/setup.md - Register an endpoint, get the secret, Sandbox testing
- references/verification.md - Svix verification details, header fallback, manual verifier, timestamp window