# 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.

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/zoop-webhooks/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.

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zoop-webhooks/examples/express/),
> [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zoop-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zoop-webhooks/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

```json
{
  "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](https://github.com/hookdeck/webhook-skills/blob/main/skills/zoop-webhooks/references/overview.md).

## Environment Variables

```bash
ZOOP_AUTHORIZATION=xxxxxxxxxxxx   # the exact authorization string set when creating the webhook

```

## Local Development

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/zoop-webhooks/references/overview.md) - Event body, event types, delivery and retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zoop-webhooks/references/setup.md) - Creating the webhook with an authorization value
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zoop-webhooks/references/verification.md) - Header check in Node, Python and Next.js, gotchas
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zoop-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests