pix-harness
v0.1.0
Published
Send signed Mercado Pago Pix webhooks to your local endpoint. One command, zero runtime dependencies.
Maintainers
Readme
pix-harness
Send a correctly signed Mercado Pago Pix webhook to your local endpoint in one command.
npx pix-harness send --to http://localhost:3000/api/webhookNo API keys. No sandbox account. No ngrok. No waiting for a real Pix to be paid.
Why this exists
Mercado Pago's webhook signature is easy to get almost right, and "almost" means
every event fails verification while your endpoint happily answers 200.
Three things break most integrations:
data.idlives in the query string, not the body — and the signature is computed over it. Read it from the body and every signature fails.- The manifest has a trailing semicolon:
${data.id};${x-request-id};${ts};Drop it and you get a mismatch with no useful error message. - Order notifications use
type=order, nottype=payment. Filter onpaymentand you drop every event while returning200— so Mercado Pago never retries and the payment is silently lost.
This tool reproduces all three exactly, so the code you debug against is the code that will run in production.
What it sends
POST /api/webhook?data.id=ord_abc123&type=order
x-request-id: req_7jakj4cf1d
x-signature: ts=1793458076,v1=f653317bada35d561df0f7f0fd757bd5c8d507a79bccc5866da5f7724404baa8
{
"type": "order",
"action": "order.processed",
"data": {
"id": "ord_abc123",
"status": "processed",
"external_reference": "ORD-001",
"total_amount": "10.50"
}
}Signature = HMAC-SHA256(secret, "ord_abc123;req_7jakj4cf1d;1793458076;") in hex.
Usage
npx pix-harness send --to <url> [options]| Flag | Default | What it does |
|---|---|---|
| --to <url> | required | Your webhook endpoint |
| --type <event> | paid | paid | expired | refunded | failed |
| --amount <cents> | 1050 | Amount in integer cents (1050 = R$10.50) |
| --order <ref> | generated | Your external_reference |
| --secret <s> | $PIX_WEBHOOK_SECRET | The webhook secret used for the HMAC |
| --request-id <id> | generated | Value of the x-request-id header |
| --duplicate | off | Send the same event twice |
| --timeout <ms> | 10000 | Request timeout |
| --help, -h | | Show help |
--duplicate is the one that matters
npx pix-harness send --to http://localhost:3000/api/webhook --duplicateMercado Pago retries webhooks. If your handler is not idempotent, a retry means
charging the customer twice. This flag delivers the identical data.id twice
so you can prove you only apply the side effect once:
your endpoint
ok attempt 1/2 - 200 (42ms) {"received":true,"appliedSideEffect":true}
ok attempt 2/2 - 200 (8ms) {"received":true,"appliedSideEffect":false}If your second response also created an order, you have a bug — and you found it here instead of in your customer's bank statement.
Verifying on your side
import { createHmac, timingSafeEqual } from 'node:crypto';
const manifest = `${dataId};${requestId};${ts};`; // trailing semicolon
const expected = createHmac('sha256', secret).update(manifest).digest('hex');
const valid = timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(v1, 'hex'),
);dataId comes from req.query['data.id'], requestId from the x-request-id
header, and ts/v1 are parsed out of x-signature.
What this tool does NOT do
Deliberate scope — this is a test harness, not an integration library:
- No ready-made webhook handler / middleware
- No idempotency store (you still have to build yours)
- No Mercado Pago API client — no creating orders, no polling, no refunds
- No real Pix QR codes
- No card payments
- No replay of captured production payloads
Those are the parts that cost days, not minutes.
When you already have the integration
If you're past the "why doesn't my signature verify" stage and into "why did this customer get charged twice", the harness won't help — the remaining work is the order lifecycle, idempotency, reconciliation and the webhook routing edge cases.
That's the Pix Webhook Kit — a production handler with the six documented Mercado Pago traps, idempotency and stores included. The harness here is free and stays free.
License
MIT © Marcelo Araujo
