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 Authorization header on a webhook?
  • Does Zoop sign webhook payloads? (No: shared authorization value only)
  • Why does my Zoop webhook stay registred instead of active? (the ping was rejected)
  • How do I handle transaction.succeeded, transaction.failed or seller.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 ping with 200. On creation Zoop sends type: "ping" and activates the webhook only if the URL accepts it. Zoop's docs don't say whether the ping carries your Authorization header; 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 200 first, then process (queue or background task).
  • Dispatch on type, dedupe on id. Zoop may deliver an event more than once (up to 3 attempts), so process each event id once.

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

EventTriggered When
pingWebhook created; Zoop checks the URL is valid and public
transaction.succeededA transaction is approved
transaction.failedA transaction fails
transaction.canceledA transaction is canceled
seller.activatedA seller (merchant) is activated
bank_account.associatedA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →