Gareth Wilson Gareth Wilson

Guide to Zoop Webhooks: Features and Best Practices

Published


Zoop is a Brazilian payments platform for marketplaces. Its webhooks tell your platform when a resource changes state: a transaction is approved, a seller passes manual onboarding, a subscription is charged, a transfer is paid. Some of these changes happen without any API call from you, so webhooks are how you find out about them.

Zoop's documentation is in Portuguese. This guide covers how Zoop webhooks are created and activated, how to authenticate them with the authorization value, and how to handle a timeout of just 1 second.

What are Zoop webhooks?

Zoop generates an event whenever a resource on your marketplace changes, whether or not you have webhooks registered. You can list events through the API, but Zoop strongly discourages crawling that API. Webhooks push each event to a URL you register for the event types you choose.

Zoop doesn't sign webhooks. When you create a webhook through the API, you can set an authorization string, and Zoop sends it back unchanged in the Authorization header of every delivery. That value is the only thing to verify. Zoop adds no Bearer prefix; if you want one, include it in the string.

Zoop webhook features

FeatureDetails
ConfigurationPOST /v1/marketplaces/{marketplace_id}/webhooks, or the Zoop Dashboard
Method and formatPOST (the only allowed value), JSON
AuthenticationThe authorization value, sent verbatim as Authorization
Signature and timestampNone
ActivationA ping event; the webhook becomes active only if it succeeds
Event selectionAn event array when you create the webhook
DispatchUp to 5 minutes after the event is generated
Timeout1 second
RetriesAfter a timeout, Zoop tries again 10 minutes later; 3 failed attempts mark the event fail
ReplayFailed events can be re-sent through the replay API for 45 days
DuplicatesPossible
Source IPsNot published

Event types

Zoop publishes 75 event types, grouped by resource. The main families are:

  • transaction.*, such as transaction.created, transaction.succeeded, transaction.failed and transaction.canceled, plus chargeback and dispute variants
  • buyer.transaction.*
  • seller.*, such as seller.activated
  • bank_account.*, such as bank_account.associated
  • subscription.*, plan.* and invoice.* for recurring billing
  • receivable.*, receivable_unit.contract.* and prepayment.*
  • transfer.*

Copy event names from Zoop's event lists exactly. At least one is published with a typo, transaction.chargeback.reverserd, so check a real delivery before you match on an unusual name.

Event payload

Every event uses the same envelope:

{
  "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"
}

type names the event, and payload.object holds the resource in its final state after the action, so its shape depends on type. The top-level id is the event ID.

Inspect Zoop webhooks as they arrive. Create a webhook with a Hookdeck Inspect URL to see the ping and the exact headers and envelope Zoop sends, with no account or setup required.

Setting up Zoop webhooks

Generate a long random value for the authorization string, for example with openssl rand -hex 32. Then create the webhook with your Zoop API credentials:

curl --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", "transaction.failed"],
    "authorization": "your_random_value"
  }'

Add your Zoop API authentication to the request as the API reference describes. The URL must be public. Zoop's dashboard can also register webhooks, but its authorization page only shows the API, so create the webhook through the API when you need the authorization value.

The ping

Right after you create a webhook, Zoop sends a ping event to check that the URL is valid and public:

{
  "id": "{{ping_id}}",
  "type": "ping",
  "resource": "event",
  "payload": { "object": { "ping": "true" } },
  "source": null,
  "name": null,
  "uri": "/v1/marketplaces/{{marketplace_id}}/events/{{event_id}}",
  "created_at": "2020-04-01T18:21:33+0000",
  "updated_at": "2020-04-01T18:21:33+0000"
}

If the ping succeeds, the webhook's status becomes active. If not, it stays registred (Zoop's spelling) and receives no events. If you see no ping within 5 minutes, list the marketplace's webhooks through the API and check status. Zoop's docs don't say whether the ping carries the Authorization header, so confirm it on a real ping (see Securing Zoop webhooks below).

With Hookdeck. Zoop is a supported source in Hookdeck. Its setup note reads: "Set an authorization value when you create the webhook through the Zoop API, then paste the same value here." Create a source with the Zoop type and use its URL as the webhook url. Paste the authorization string into Authorization Header exactly as you set it, including any prefix you added. Hookdeck compares the whole header value.

The source accepts POST only, which is the only method Zoop allows. A request with a missing or wrong header still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service. Hookdeck answers the ping with a 200 like any other POST. After you create the webhook, check that its status is active.

For local development, hookdeck listen 3000 zoop --path /webhooks/zoop gives you a public URL that forwards to your machine.

Securing Zoop webhooks

Compare the whole Authorization header with the value you set, in constant time, before you parse the body. Don't strip a prefix or decode anything.

const crypto = require('crypto');
const express = require('express');

const EXPECTED = process.env.ZOOP_AUTHORIZATION;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/zoop', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get('authorization');
  if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  if (event.type === 'ping') return res.sendStatus(200);

  // Zoop waits 1 second: queue the event, then acknowledge.
  res.sendStatus(200);
});

The same check in Python:

import hmac
import os

EXPECTED = os.environ.get("ZOOP_AUTHORIZATION", "")


def verify_zoop_request(headers) -> bool:
    if not EXPECTED:
        return False  # never accept when unconfigured
    received = headers.get("Authorization")
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))

If you receive Zoop webhooks directly and the ping arrives without the header, this check rejects it and the webhook stays registred. If that happens, capture the ping in the Console to see what Zoop sends before you change the check.

A static value proves the sender knows it and nothing more. It doesn't cover the body, so a modified payload still passes, and with no timestamp a captured request can be replayed. Zoop doesn't publish source IPs, so the value is the only check you have. Use HTTPS only, use a long random value, and create the webhook again with a new value if it leaks.

Make Zoop webhooks production-ready. Hookdeck Event Gateway acknowledges Zoop at the edge before any of your code runs, checks the authorization value, and stores every event durably before it reaches your service.

Zoop webhook limitations and pain points

A 1-second timeout

The Problem: A handler that does any real work, or a cold start, misses Zoop's deadline and the delivery counts as failed.

Why It Happens: Zoop waits 1 second for a response, then closes the connection and tries again 10 minutes later.

Workarounds:

  • Do nothing in the request but check the header, store the event and return 200. Zoop itself says your receiver must store events in a queue and process them later.
  • Keep the endpoint warm if it runs on serverless infrastructure.

How Hookdeck Can Help: Hookdeck responds to Zoop at the edge and delivers to your service afterwards with a delivery timeout you control, so slow handlers no longer cost you deliveries.

Three attempts, then manual replay

The Problem: After 3 failed attempts the event is marked fail and Zoop stops sending it.

Why It Happens: Zoop's retry flow is short. Failed events stay available for 45 days, but only through the replay API.

Workarounds:

  • Call the replay API (POST /v1/marketplaces/{marketplace_id}/events/actions/replay) for failed events after an outage.
  • Reconcile important resources, such as transactions, against the API.

How Hookdeck Can Help: Hookdeck stores each event when it arrives. Retries to your service follow a schedule you set, you can bulk-replay failures from the dashboard, and issues alert you as soon as deliveries start failing.

Delayed and duplicate deliveries

The Problem: An event arrives minutes after the change, and sometimes more than once.

Why It Happens: Zoop waits up to 5 minutes before dispatching, so your system can finish work after the API call that caused the event. Its docs also promise delivery within 10 minutes, and retries add further delay. Zoop warns that events can be sent more than once.

Workarounds:

  • Don't rely on webhooks for anything that must happen within seconds of an API call; use the API response.
  • Process each event id once.

How Hookdeck Can Help: Hookdeck deduplication can drop repeats based on the event id.

No signature

The Problem: You can't tell whether a body has been changed.

Why It Happens: Zoop's only authentication is the static authorization value.

Workarounds:

  • Treat payload.object as a hint for anything involving money, and fetch the resource from the API before acting on it.

How Hookdeck Can Help: Hookdeck keeps requests without the right value away from your service and records them as failed verification, so you can see attempts and misconfigured webhooks.

Best practices

Subscribe only to what you handle

List just the event types you process in the event array. Return 200 for any type you don't recognise so it isn't retried.

Dispatch on type and deduplicate on ID

Route each event by type and record each id once it's processed. See our guide to webhook idempotency.

Check activation after every change

After you create a webhook, list your marketplace's webhooks and confirm the status is active. A webhook stuck at registred receives no events.

Queue before you process

With a 1-second timeout, asynchronous processing isn't optional. See why to process webhooks asynchronously.

Conclusion

Zoop webhooks report changes across transactions, sellers, billing and transfers in one envelope. The ping decides whether a webhook works at all, and the 1-second timeout leaves no time for work inside the request. Set a long random authorization value, compare the whole header in constant time, confirm the webhook is active, and queue every event before you process it.

Hookdeck Event Gateway answers Zoop instantly, checks the authorization value, and lets you retry and replay delivery to your platform.

Get started with Hookdeck for free and handle Zoop webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.