Agent skill
Celcoin Webhooks Skill
Receive and authenticate Celcoin webhooks (Brazilian BaaS: Pix, TED, accounts, onboarding). Use when setting up a Celcoin webhook endpoint, verifying the HTTP Basic auth credentials Celcoin sends, debugging 401s, or handling events like pix-payment-in, pix-payment-out, pix-reversal-in, internal-transfer-in, spb-transfer-out, onboarding-proposal and kyc.
Install this skill
npx skills add hookdeck/webhook-skills --skill celcoin-webhooks
Celcoin (a Brazilian banking-as-a-service platform: Pix, TED, accounts, onboarding; docs are in Portuguese) sends a JSON webhook when something happens, for example a Pix is received or an onboarding proposal is decided. Subscriptions are created through the Celcoin API, one per entity. Webhooks are not signed. Your endpoint authenticates the sender with HTTP Basic auth: the login and pwd you set in the subscription's auth object come back on every notification as Authorization: Basic base64(login:pwd).
Hookdeck setup note: Use Basic auth (auth.type basic) on the webhook subscription, not JWT/OAuth 2.0.
Celcoin also offers a JWT/OAuth 2.0 mode (auth.type jwt, where Celcoin first calls your token endpoint). Hookdeck's Celcoin source does not cover it, and neither does this skill.
When to Use This Skill
- How do I receive Celcoin webhooks?
- How do I authenticate Celcoin webhooks with Basic auth?
- Does Celcoin sign webhook payloads? (No: static credentials only)
- How do I handle
pix-payment-in,pix-payment-out,spb-transfer-outoronboarding-proposalevents? - Why does my Celcoin webhook endpoint return 401?
Verification (core)
const crypto = require('crypto');
const sha = (s) => crypto.createHash('sha256').update(s, 'utf8').digest();
const safeEqual = (a, b) => crypto.timingSafeEqual(sha(a), sha(b)); // equal-length digests
function verifyCelcoinAuth(authorization, username, password) {
if (!username || !password || typeof authorization !== 'string') return false;
const m = /^basic[ \t]+(\S+)$/i.exec(authorization.trim()); // scheme is case-insensitive
if (!m || !/^[A-Za-z0-9+/]+={0,2}$/.test(m[1]) || m[1].length % 4) return false; // strict base64
const decoded = Buffer.from(m[1], 'base64').toString('utf8');
const i = decoded.indexOf(':'); // split on the FIRST colon
if (i === -1) return false;
const userOk = safeEqual(decoded.slice(0, i), username);
const passOk = safeEqual(decoded.slice(i + 1), password); // evaluate both, no short-circuit
return userOk && passOk;
}
- Header:
Authorization: Basic base64(username:password). Read the credentials fromCELCOIN_WEBHOOK_USERNAMEandCELCOIN_WEBHOOK_PASSWORD. - Decode strictly (base64 alphabet only;
validate=Truein Python) so valid credentials followed by junk are rejected. The full examples also re-encode and compare in Node. - Compare the username and the password each in constant time and evaluate both before deciding. Python:
hmac.compare_digeston bytes (encode both sides;strraises on non-ASCII). - Return
401for a missing or malformed header or a mismatch,400for a body that isn't a JSON object,200once handled. - Hookdeck never sends a 401 challenge (no
WWW-Authenticateround trip), so the credentials must arrive with every request. - A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS, and rotate the credential (update the subscription) if it leaks.
- Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing it.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
HTTP Methods
Celcoin's docs don't name the HTTP method of the notification. The examples therefore accept GET, POST, PUT, PATCH and DELETE on the webhook route, check the credential first for every method, and acknowledge an empty body. Hookdeck's Celcoin source pre-selects POST without managing the method, so if Celcoin uses any other method, also allow that method on the Hookdeck source.
Event Payload and Entities
Every notification has the same envelope, and you dispatch on entity:
{
"entity": "pix-payment-in",
"createTimestamp": "2025-05-07T17:30:00.7647117",
"status": "CONFIRMED",
"body": { "id": "e2726e30-c318-4e1f-b284-e37c14e6c4b2" }
}
status is CONFIRMED or ERROR (an ERROR notification also carries an error object with errorCode and message). Subscriptions are registered per entity:
| Entity | Sent When |
|---|---|
pix-payment-out | A Pix payment or transfer you sent is confirmed |
pix-payment-in | A Pix is received |
pix-reversal-out | A refund of a received Pix you issued is confirmed |
pix-reversal-in | A refund of a Pix-out you made is received |
internal-transfer-in | An internal transfer is received |
internal-transfer-out | An internal transfer is sent |
spb-transfer-out | A TED you sent is confirmed |
spb-transfer-in | A TED is received |
onboarding-create | A new account is created |
onboarding-proposal | The onboarding proposal is decided (Approved or Reproved) |
kyc | The KYC result of an account is available |
Celcoin documents many more entities (Pix Automático, DICT claims, charges, bill payments, cards). See references/overview.md.
Environment Variables
CELCOIN_WEBHOOK_USERNAME=your_webhook_login # auth.login on the subscription
CELCOIN_WEBHOOK_PASSWORD=your_webhook_pwd # auth.pwd on the subscription
Local Development
Celcoin can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 celcoin --path /webhooks/celcoin
Use the printed URL as the webhookUrl of the subscription.
Resources
- references/overview.md - What Celcoin webhooks are, entities and envelope
- references/setup.md - Register a subscription with Basic auth
- references/verification.md - Basic auth check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests