Guide to Celcoin Webhooks: Features and Best Practices
Celcoin webhooks tell your platform about money and account events in Brazil's financial infrastructure: a Pix arrives or is sent, a TED is confirmed, an onboarding proposal is approved, a service invoice is issued. If you build on Celcoin's banking-as-a-service and payments APIs, webhooks are how those asynchronous results reach your systems. Celcoin's documentation is in Portuguese, and the quotations below are translated.
This guide covers how Celcoin webhook subscriptions are registered and authenticated, the events Celcoin documents and the shape of a notification, how to recover missed webhooks, and the practices that keep a Celcoin receiver reliable.
What are Celcoin webhooks?
Celcoin has no webhook settings page. You register webhooks through its API, one subscription per event, which Celcoin calls an entity: pix-payment-in for received Pix, spb-transfer-out for a TED you sent, onboarding-proposal for an account decision, and so on. Each subscription has a URL and an authentication setting, and you can list, update and delete subscriptions through the same API.
There are two modules for this. The common module, used for Pix, NFS-e, BaaS accreditation and SPB, takes a context naming the product as well as the entity. The BaaS module manages webhooks for Celcoin's banking-as-a-service accounts.
Celcoin does not sign webhooks. Each subscription chooses one of two ways to authenticate notifications:
- Basic Authentication: you set a
loginandpwd, and "the defined basic auth will be sent in the notifications". - OAuth 2.0 (JWT): you run a token endpoint, and before sending a notification Celcoin requests a JWT from it and uses that token to authenticate to your URL.
Hookdeck's Celcoin source checks Basic Authentication.
Celcoin webhook features
| Feature | Details |
|---|---|
| Configuration | API only: common module (/common/v1/webhook/subscription) or BaaS module (/baas/v2/webhook/subscription) |
| Subscription model | One per entity (event) and URL |
| Format | JSON |
| HTTP method | Not stated in the webhook docs |
| Authentication | Per subscription: Basic (auth.type basic, login, pwd) or JWT/OAuth 2.0 |
| Signature and timestamp | None |
| Event type | entity |
| Body | entity, createTimestamp, status (CONFIRMED or ERROR), body, and error on failures |
| Resend | Resend and sent-webhook details for up to 7 days after the transaction |
| Retries, timeout, source IPs | Not documented |
| Contract changes | Fields are never removed without notice, but new fields can be added at any time |
Common events
Celcoin documents around 57 entities across its products. These are the ones most integrations start with:
| Product | Entities |
|---|---|
| Pix | pix-payment-out, pix-payment-in, pix-reversal-out, pix-reversal-in |
| Pix keys and disputes | pix-dict-claim-open, pix-dict-claim-confirmed, pix-dict-claim-completed, pix-infraction, pix-med-refund |
| Pix Automático | pix-automatic-recurrency-awaiting-debtor, pix-automatic-recurrency-completed, pix-automatic-payment-instruction-cashin, pix-automatic-payment-instruction-completed |
| TED (SPB) | spb-transfer-out, spb-transfer-in, spb-reversal-in, spb-event-error |
| Internal transfers | internal-transfer-in, internal-transfer-out |
| Onboarding and KYC | onboarding-create, onboarding-proposal, onboarding-backgroundcheck, onboarding-documentscopy, kyc |
| Service invoices | invoice-nfse-registration, invoice-nfse-cancellation |
| Accounts | account-accreditation, account-status |
The BaaS module also covers charges (boleto), bill payments, top-ups, vehicle debts, card settlements through SLC, court-ordered blocks and manual treasury entries.
The BaaS templates API returns example notifications for each entity. This one, trimmed, is the template for a confirmed outgoing TED:
{
"entity": "spb-transfer-out",
"createTimestamp": "2023-07-24T17:02:47.0824409",
"status": "CONFIRMED",
"body": {
"clientFinality": "10",
"amount": 750,
"clientCode": "78d96006-c535-46e3-93a6-d8c85a6000bc",
"description": "Pagamento",
"id": "7e172eef-f05e-429e-ae36-401310a204cd",
"debitParty": { "bank": "13935893", "branch": "0001", "account": "300123", "accountType": "CC" },
"creditParty": { "bank": "00360305", "branch": "0001", "account": "300123", "accountType": "CC" }
}
}
An ERROR template adds an error object with errorCode and message. These are templates returned by an API, not captured deliveries, so check a real notification from your own subscription before you rely on the envelope.
Inspect Celcoin webhooks as they arrive. Register a sandbox subscription with a Hookdeck Inspect URL as
webhookUrlto see exactly what Celcoin sends, with no account or setup required.
Setting up Celcoin webhooks
Choose a username and a long random password first, for example with openssl rand -hex 32 for the password.
- Authenticate to the Celcoin API to get an access token.
- Register a subscription for each entity you need, with
auth.typeset tobasic:
curl -X POST "https://sandbox.openfinance.celcoin.dev/common/v1/webhook/subscription" \
-H "accept: application/json" \
-H "content-type: application/json" \
-H "Authorization: Bearer $CELCOIN_ACCESS_TOKEN" \
-d '{
"context": "PIX",
"entity": "pix-payment-in",
"auth": { "type": "basic", "login": "celcoin", "pwd": "<long random password>" },
"webhookUrl": "https://example.com/webhooks/celcoin"
}'
For BaaS accounts, send the same entity, webhookUrl and auth to /baas/v2/webhook/subscription, without context.
- Repeat for each entity. If you already have a subscription for an entity, registering it again can fail with error
CBE205, so update the existing one withPUTinstead.
To route through Hookdeck, create a Hookdeck source with the Celcoin type and use its URL as webhookUrl. Use Basic auth (auth.type basic) on the webhook subscription, not JWT/OAuth 2.0. The source asks for a Username and Password: enter the same login and pwd. Hookdeck never answers with a 401 challenge, so the credentials must arrive with each notification, which is how Celcoin describes Basic mode.
Celcoin's webhook docs do not say which HTTP method notifications use, so the Hookdeck source pre-selects POST without managing the method. If your notifications arrive with another method, add it to the source's allowed HTTP methods.
Securing Celcoin webhooks
With Basic Authentication, every notification should carry Authorization: Basic <base64(login:pwd)>. Decode it, split on the first colon, and compare the username and password with your stored values in constant time.
const crypto = require('crypto');
const express = require('express');
const USERNAME = process.env.CELCOIN_WEBHOOK_USERNAME;
const PASSWORD = process.env.CELCOIN_WEBHOOK_PASSWORD;
function safeEqual(a, b) {
// timingSafeEqual throws on unequal lengths, so check first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
function verifyBasicAuth(header) {
if (!USERNAME || !PASSWORD || typeof header !== 'string') return false;
const match = /^basic\s+([A-Za-z0-9+/]+={0,2})\s*$/i.exec(header);
if (!match) return false;
const decoded = Buffer.from(match[1], 'base64');
const colon = decoded.indexOf(0x3a); // split on the first colon
if (colon === -1) return false;
const userOk = safeEqual(decoded.subarray(0, colon), Buffer.from(USERNAME, 'utf8'));
const passOk = safeEqual(decoded.subarray(colon + 1), Buffer.from(PASSWORD, 'utf8'));
return userOk && passOk;
}
const app = express();
app.post('/webhooks/celcoin', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyBasicAuth(req.get('authorization'))) {
return res.sendStatus(401);
}
let notification;
try {
notification = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// Route on notification.entity and notification.status; store before processing.
res.sendStatus(200);
});
The same check in Python:
import base64
import binascii
import hmac
import os
USERNAME = os.environ.get("CELCOIN_WEBHOOK_USERNAME", "")
PASSWORD = os.environ.get("CELCOIN_WEBHOOK_PASSWORD", "")
def verify_basic_auth(header) -> bool:
if not USERNAME or not PASSWORD or not header:
return False
scheme, _, encoded = header.partition(" ")
if scheme.lower() != "basic":
return False
try:
decoded = base64.b64decode(encoded.strip(), validate=True)
except (binascii.Error, ValueError):
return False
user, sep, password = decoded.partition(b":")
if not sep:
return False
user_ok = hmac.compare_digest(user, USERNAME.encode("utf-8"))
pass_ok = hmac.compare_digest(password, PASSWORD.encode("utf-8"))
return user_ok and pass_ok
Basic credentials are static. They authenticate the sender, not the body, so a modified payload still passes, and with no timestamp a captured request can be replayed. Serve the endpoint over HTTPS only, use a long random password, and update the subscription and your environment together if it might have leaked. For money movement, confirm the transaction through Celcoin's API before acting on a notification.
Make Celcoin webhooks production-ready. Hookdeck Event Gateway checks the Basic credentials on every notification, queues each Pix, TED and onboarding event durably, and retries delivery to your service on a schedule you control.
Celcoin webhook limitations and pain points
Delivery behaviour is undocumented
The Problem: You cannot tell from the docs how long Celcoin waits for your endpoint, whether it retries, or what happens when your service is down.
Why It Happens: Celcoin's webhook pages cover registration and resending, not the delivery attempt itself.
Workarounds:
- Respond quickly and keep the endpoint highly available.
- Use the resend API, which works for up to 7 days after a transaction, to recover anything you missed. After that, use the transaction status APIs.
- Reconcile against Celcoin's account statement, which Celcoin says is the official record. It warns against building statements from webhooks.
How Hookdeck Can Help: Hookdeck accepts each notification as soon as it arrives and stores it, then delivers to your service with retries you configure. You can replay any notification after a fix, and issues alert you when deliveries to your service fail.
Only Basic mode can be checked
The Problem: A subscription set up with JWT/OAuth 2.0 never verifies in Hookdeck's Celcoin source.
Why It Happens: In OAuth 2.0 mode, Celcoin fetches a fresh token from your own token endpoint before each notification, so there is no fixed credential to compare. The Celcoin source checks Basic credentials only.
Workarounds:
- Use
auth.typebasicfor subscriptions that point at Hookdeck. - If you need the JWT mode, validate the token in your own receiver against the endpoint that issued it.
How Hookdeck Can Help: With Basic mode, the source compares the username and password you enter. Hookdeck answers Celcoin with a 200 either way, but requests without matching credentials are recorded as failed verification and never reach your service.
Many subscriptions to keep in step
The Problem: A credential rotation or URL change misses some events.
Why It Happens: Every entity is its own subscription with its own auth object, and there are dozens of entities.
Workarounds:
- Script subscription management: list subscriptions, then update each with
PUTwhen the URL or credentials change. - Point all subscriptions at one URL and route on
entityinside your service.
How Hookdeck Can Help: One Hookdeck source can receive every entity and route each to the right service with filters on entity, so your Celcoin subscriptions never need to change when your internal routing does.
New fields appear without notice
The Problem: A strict schema validator starts rejecting notifications after a Celcoin release.
Why It Happens: Celcoin promises not to remove fields from a webhook contract without notice, but says new fields can be added whenever needed, and asks customers not to reject webhooks that contain them.
Workarounds:
- Validate only the fields you use, and ignore the rest.
- Log unknown fields so you notice when the contract grows.
How Hookdeck Can Help: Hookdeck keeps every notification with its full body, so you can see a new field the day it appears and replay notifications after updating your handler.
Best practices
One URL, route on entity
Register every subscription to the same endpoint and switch on entity, then status. It keeps rotations and migrations to a single change.
Treat ERROR as a result, not a failure
An ERROR notification tells you an operation failed, such as an outgoing TED. Return 200 for it, then handle the business failure.
Make handlers idempotent
Store each notification keyed on entity and the body.id before acting on it. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
Check the credentials, store the notification, return 200, and do the work afterwards. See why to process webhooks asynchronously.
Conclusion
Celcoin registers webhooks per event through its API and sends each notification as JSON with the subscription's Basic credentials and no signature. Use auth.type basic with a long random password, compare the decoded credentials in constant time over HTTPS, route on entity and status, tolerate new fields, and plan to recover missed notifications with the resend API within 7 days, because delivery behaviour is not documented.
Hookdeck Event Gateway checks the Basic credentials, keeps every notification, and retries and replays delivery to your service, so a short outage on your side does not leave a gap in your Pix and TED records.
Get started with Hookdeck for free and handle Celcoin webhooks reliably in minutes.