# Webhooks Webhooks deliver real-time notifications when resources change. Instead of polling the API, register a URL and receive `POST` requests whenever events occur. The Dinie API follows the [Standard Webhooks](https://www.standardwebhooks.com/) specification. ## Configuring Webhook Endpoints ### Create an Endpoint Register a URL to receive webhook events. You can subscribe to specific events or receive all of them. ```typescript Node.js const endpoint = await dinie.webhooks.endpoints.create({ url: "https://yourapp.example.com/webhooks/dinie", events: ["customer.active", "credit_offer.available", "loan.*"], description: "Production webhook", }); // Store endpoint.secret securely -- it is only displayed once. console.log(endpoint.secret); // "whsec_xxxxx..." ``` ```ruby Ruby endpoint = dinie.webhooks.endpoints.create( url: "https://yourapp.example.com/webhooks/dinie", events: ["customer.active", "credit_offer.available", "loan.*"], description: "Production webhook" ) # Store endpoint.secret securely -- it is only displayed once. puts endpoint.secret # "whsec_xxxxx..." ``` ```python Python endpoint = dinie.webhooks.endpoints.create( url="https://yourapp.example.com/webhooks/dinie", events=["customer.active", "credit_offer.available", "loan.*"], description="Production webhook", ) # Store endpoint.secret securely -- it is only displayed once. print(endpoint.secret) # "whsec_xxxxx..." ``` ```bash cURL curl -X POST https://sandbox.api.dinie.com.br/v3/webhooks/endpoints \ -H "Authorization: Bearer dinie_at_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.example.com/webhooks/dinie", "events": ["customer.active", "credit_offer.available", "loan.*"], "description": "Production webhook" }' ``` The returned `endpoint.secret` is required to verify signatures -- store it immediately in your secrets manager. > **Warning:** The `secret` is **only returned on creation and rotation**. Store it immediately in your secrets manager. You need it to verify webhook signatures. ### Managing Endpoints | Operation | Method | Endpoint | | --- | --- | --- | | List all | `GET` | `/v3/webhooks/endpoints` | | Get one | `GET` | `/v3/webhooks/endpoints/{id}` | | Update | `PATCH` | `/v3/webhooks/endpoints/{id}` | | Delete | `DELETE` | `/v3/webhooks/endpoints/{id}` | You can disable an endpoint without deleting it by setting `status: "disabled"` via `PATCH`. ## Receiving Webhooks Each delivery is a `POST` request with authentication headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`). Use the SDK to verify and parse the event: ```typescript Node.js import express from "express"; import Dinie from "dinie"; const app = express(); const dinie = new Dinie({ clientId: process.env.DINIE_CLIENT_ID, clientSecret: process.env.DINIE_CLIENT_SECRET, webhookSecret: process.env.DINIE_WEBHOOK_SECRET, }); app.post("/webhooks/dinie", express.raw({ type: "application/json" }), (req, res) => { const event = dinie.webhooks.unwrap(req.body.toString(), req.headers); switch (event.type) { case "customer.active": console.log(`Customer ${event.data.id} approved`); break; case "loan.active": console.log(`Loan ${event.data.id} disbursed`); break; } res.sendStatus(200); }); ``` ```ruby Ruby class WebhooksController < ApplicationController skip_before_action :verify_authenticity_token def create event = dinie.webhooks.unwrap(request.raw_post, request.headers) case event.type when "customer.active" puts "Customer #{event.data.id} approved" when "loan.active" puts "Loan #{event.data.id} disbursed" end head :ok end end ``` ```python Python import os from flask import Flask, request from dinie import Dinie app = Flask(__name__) dinie = Dinie( client_id=os.environ["DINIE_CLIENT_ID"], client_secret=os.environ["DINIE_CLIENT_SECRET"], webhook_secret=os.environ["DINIE_WEBHOOK_SECRET"], ) @app.route("/webhooks/dinie", methods=["POST"]) def handle_webhook(): event = dinie.webhooks.unwrap( request.get_data(as_text=True), request.headers, ) if event.type == "customer.active": print(f"Customer {event.data.id} approved") elif event.type == "loan.active": print(f"Loan {event.data.id} disbursed") return "", 200 ``` The `webhook_secret` is configured in the client constructor. The `unwrap` method uses this secret to verify the HMAC-SHA256 signature, validate the timestamp (5-minute replay protection), and return the typed event. If verification fails, an exception is thrown -- return `400` in that case. ## Manual Signature Verification If you are not using the SDKs, implement verification manually following the [Standard Webhooks](https://www.standardwebhooks.com/) specification: 1. Build the signed content: `{webhook-id}.{webhook-timestamp}.{raw_body}` 2. Decode the secret: remove the `whsec_` prefix and base64-decode the remainder 3. Compute the HMAC-SHA256 of the signed content using the decoded secret 4. Base64-encode the result and compare it against each signature in the `webhook-signature` header 5. Reject if the `webhook-timestamp` is older than 5 minutes (replay protection) See the [Standard Webhooks](https://www.standardwebhooks.com/) specification for full details on headers and signature format. ## Retry Policy Failed deliveries (non-2xx responses or timeouts) are retried with exponential backoff: | Attempt | Interval | | --- | --- | | 1 | 1 minute | | 2 | 5 minutes | | 3 | 15 minutes | | 4 | 1 hour | | 5 | 6 hours | After 5 failed attempts, the event is marked as `failed`. Repeated failures across multiple events will automatically disable the webhook endpoint. > **Info:** Use the `webhook-id` header as an idempotency key to deduplicate deliveries on your side. The same event may be delivered more than once during retries. ## Secret Rotation Rotate your webhook signing secret without downtime: ```bash curl -X POST https://sandbox.api.dinie.com.br/v3/webhooks/endpoints/we_550e8400.../secret/rotate \ -H "Authorization: Bearer dinie_at_..." \ -H "Content-Type: application/json" \ -d '{ "expire_current_in": 3600 }' ``` During the transition period (`expire_current_in`, default 1 hour, maximum 24 hours), both secrets -- old and new -- are active. The `webhook-signature` header contains signatures for **both secrets**, so your verification code will match either one. 1. Call `POST /v3/webhooks/endpoints/{id}/secret/rotate` with a transition period 2. Update your application with the new secret from the response 3. Deploy -- during the transition period, both secrets work 4. After the transition period, only the new secret is used ## Best Practices 1. **Always verify signatures** -- never trust webhook payloads without verification 2. **Respond quickly** -- return a `200` status within 5 seconds. Process the event asynchronously if needed. 3. **Implement idempotency** -- use the `webhook-id` to deduplicate. Your handler should safely process the same event twice. 4. **Use HTTPS** -- webhook URLs must use HTTPS. HTTP endpoints are rejected. 5. **Log deliveries** -- store the `webhook-id` and `webhook-timestamp` for debugging and audit trails. ## Available Events All payloads follow the same envelope: ```json { "type": "resource.action", "timestamp": "2026-03-04T10:00:00Z", "data": { ... } } ``` ### Wildcard Subscription - Specific events: `["customer.active", "loan.created"]` - All from a resource: `["loan.*"]` - All events: omit the `events` field or pass an empty array ### Events by Resource | Resource | Event | Description | | --- | --- | --- | | **Customer** | `customer.created` | New customer registered | | | `customer.under_review` | KYC submitted, review started | | | `customer.kyc_updated` | KYC requirement status changed | | | `customer.active` | Fully verified, eligible for credit | | **Credit Offer** | `credit_offer.available` | New offer created for the customer | | | `credit_offer.expired` | Offer passed its expiration date | | **Loan** | `loan.created` | Loan application accepted | | | `loan.contract_generated` | CCB contract generated | | | `loan.awaiting_signatures` | `signing_url` available for signing | | | `loan.signed` | Contract fully signed | | | `loan.disbursing` | Fund transfer initiated | | | `loan.active` | Funds disbursed, repayment started | | | `loan.payment_received` | Installment received | | | `loan.finished` | Loan fully repaid | | | `loan.cancelled` | Loan cancelled | | | `loan.error` | Processing or disbursement error |