# 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 `login` and `pwd`, 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:

```json
{
  "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](https://inspect.hookdeck.com) URL as `webhookUrl` to 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.

1. Authenticate to the Celcoin API to get an access token.
2. Register a subscription for each entity you need, with `auth.type` set to `basic`:

```bash
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`.

1. 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 with `PUT` instead.

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.

```javascript
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:

```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](/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](/docs/retries) you configure. You can replay any notification after a fix, and [issues](/docs/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.type` `basic` for 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 `PUT` when the URL or credentials change.
* Point all subscriptions at one URL and route on `entity` inside your service.

**How Hookdeck Can Help:** One Hookdeck source can receive every entity and route each to the right service with [filters](/docs/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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Celcoin webhooks reliably in minutes.