Agent skill
Zoop Webhooks Skill
Receive and authenticate Zoop webhooks (Brazilian payments platform). Use when setting up a Zoop webhook endpoint, checking the Authorization header you set when creating the webhook, handling the ping validation event, debugging 401s, or handling transaction.succeeded, transaction.failed, seller.activated and bank_account.associated events.
Install this skill
npx skills add hookdeck/webhook-skills --skill zoop-webhooks
Zoop (Brazilian payments platform, docs in Portuguese) sends an HTTP POST to your URL when a resource changes: a transaction succeeds, a seller is approved, a bank account is associated. Payloads are not signed. The only authentication is an authorization string you choose when you create the webhook through the Zoop API; Zoop sends it back verbatim in the Authorization header.
When to Use This Skill
- How do I receive Zoop webhooks?
- How do I verify the Zoop
Authorizationheader on a webhook? - Does Zoop sign webhook payloads? (No: shared authorization value only)
- Why does my Zoop webhook stay
registredinstead ofactive? (thepingwas rejected) - How do I handle
transaction.succeeded,transaction.failedorseller.activated?
Setup
Set an authorization value when you create the webhook through the Zoop API, then paste the same value here.
curl --location --request POST "https://api.zoop.ws/v1/marketplaces/$MARKETPLACE_ID/webhooks" \
--header 'Content-Type: application/json' \
--data-raw '{ "url": "https://example.com/webhooks/zoop", "method": "POST",
"event": ["transaction.succeeded"], "authorization": "xxxxxxxxxxxx" }'
Details (auth, the marketplace id, which events to subscribe to) are in references/setup.md.
Verification (core)
Zoop sends the value exactly as you set it: Authorization: xxxxxxxxxxxx. There is no Bearer prefix unless you included one in the string. Store the whole value as ZOOP_AUTHORIZATION and compare the whole header to it. Don't strip, parse or base64-decode anything.
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyZoopRequest(headers, expected = process.env.ZOOP_AUTHORIZATION) {
if (!expected) return false; // never accept when unconfigured
const received = headers['authorization']; // full value, as set on the webhook
return Boolean(received) && safeEqual(received, expected);
}
Return 401 for a missing or mismatched header. Python: hmac.compare_digest on .encode()d bytes of both sides (never str, which raises on non-ASCII).
A static shared credential authenticates the sender, not the body. There is no signature and no timestamp, so it gives no replay protection and does not prove the payload was not altered. Serve the endpoint over HTTPS only, treat the value like a password, and rotate it (create the webhook again with a new
authorization) if it leaks.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Handler Rules
- POST only. Zoop delivers with
POST. - Read the raw body (
express.raw({ type: '*/*' })) so any content type works, check the header first, and parse the JSON only after the check passes. - Answer
pingwith200. On creation Zoop sendstype: "ping"and activates the webhook only if the URL accepts it. Zoop's docs don't say whether the ping carries yourAuthorizationheader; the examples require it, so confirm on a real ping (a 401 in your logs means it arrived without the header). - Respond within 1 second. Zoop closes the connection after 1 second and retries after 10 minutes. Acknowledge with
200first, then process (queue or background task). - Dispatch on
type, dedupe onid. Zoop may deliver an event more than once (up to 3 attempts), so process each eventidonce.
Event Body
{
"id": "{{event_id}}",
"resource": "event",
"type": "transaction.succeeded",
"source": "API",
"payload": { "object": { } },
"uri": "/v1/marketplaces/{{marketplace_id}}/events/{{event_id}}",
"created_at": "2015-06-17T20:12:25",
"updated_at": "2015-06-18T07:45:02"
}
payload.object is the resource in its final state and varies with type.
Common Event Types
| Event | Triggered When |
|---|---|
ping | Webhook created; Zoop checks the URL is valid and public |
transaction.succeeded | A transaction is approved |
transaction.failed | A transaction fails |
transaction.canceled | A transaction is canceled |
seller.activated | A seller (merchant) is activated |
bank_account.associated | A bank account is associated |
More in references/overview.md.
Environment Variables
ZOOP_AUTHORIZATION=xxxxxxxxxxxx # the exact authorization string set when creating the webhook
Local Development
npx hookdeck-cli listen 3000 zoop --path /webhooks/zoop
No account required. Use the printed URL as the webhook url when you create the webhook.
Resources
- references/overview.md - Event body, event types, delivery and retries
- references/setup.md - Creating the webhook with an authorization value
- references/verification.md - Header check in Node, Python and Next.js, gotchas
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests