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-out or onboarding-proposal events?
  • 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 from CELCOIN_WEBHOOK_USERNAME and CELCOIN_WEBHOOK_PASSWORD.
  • Decode strictly (base64 alphabet only; validate=True in 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_digest on bytes (encode both sides; str raises on non-ASCII).
  • Return 401 for a missing or malformed header or a mismatch, 400 for a body that isn't a JSON object, 200 once handled.
  • Hookdeck never sends a 401 challenge (no WWW-Authenticate round 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:

EntitySent When
pix-payment-outA Pix payment or transfer you sent is confirmed
pix-payment-inA Pix is received
pix-reversal-outA refund of a received Pix you issued is confirmed
pix-reversal-inA refund of a Pix-out you made is received
internal-transfer-inAn internal transfer is received
internal-transfer-outAn internal transfer is sent
spb-transfer-outA TED you sent is confirmed
spb-transfer-inA TED is received
onboarding-createA new account is created
onboarding-proposalThe onboarding proposal is decided (Approved or Reproved)
kycThe 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →