Skip to main content
The TED plugin sends webhooks to notify your application about relevant events, such as transfer completions, failures, and receipts. This allows you to react in real-time without repeatedly querying the API.

Configuration


Configure the webhook in your organization:
FieldDescription
webhookUrlHTTPS URL that will receive the events
webhookSecretSecret key for signature validation
Configuration is done per organization, allowing each tenant to have its own endpoint.

Configuration example

Example — replace with your actual credentials:
{
  "webhookUrl": "https://api.yourcompany.com/webhooks/ted",
  "webhookSecret": "<YOUR_WEBHOOK_SECRET>"
}

Event types


The plugin emits four event types:
EventTriggered when
transfer.completedTED OUT or P2P transfer completed
transfer.failedTransfer failed or was rejected
transfer.incomingTED IN transfer received and credited
transfer.cancelledTransfer cancelled by user

Payload structure


All webhooks follow the same base structure:
{
  "event": "transfer.completed",
  "timestamp": "2026-01-21T14:35:00-03:00",
  "organizationId": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    // event-specific data
  }
}
FieldTypeDescription
eventStringEvent type
timestampISO 8601When the event occurred
organizationIdUUIDOrganization the event belongs to
dataObjectEvent-specific data

Detailed events


transfer.completed

Sent when a TED OUT or P2P transfer is completed.
{
  "event": "transfer.completed",
  "timestamp": "2026-01-21T14:35:00-03:00",
  "organizationId": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "transferId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "type": "TED_OUT",
    "status": "COMPLETED",
    "confirmationNumber": "20260121001",
    "controlNumber": "JD20260121000001",
    "amount": 1500.00,
    "feeAmount": 8.50,
    "totalAmount": 1508.50,
    "sender": {
      "accountId": "550e8400-e29b-41d4-a716-446655440000",
      "holderName": "João Santos"
    },
    "recipient": {
      "ispb": "00000000",
      "branch": "0001",
      "account": "123456",
      "holderName": "Maria Silva"
    },
    "completedAt": "2026-01-21T14:35:00-03:00"
  }
}

transfer.failed

Sent when a transfer fails or is rejected.
{
  "event": "transfer.failed",
  "timestamp": "2026-01-21T14:40:00-03:00",
  "organizationId": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "transferId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
    "type": "TED_OUT",
    "status": "REJECTED",
    "amount": 2000.00,
    "feeAmount": 8.50,
    "totalAmount": 2008.50,
    "sender": {
      "accountId": "550e8400-e29b-41d4-a716-446655440000",
      "holderName": "João Santos"
    },
    "recipient": {
      "ispb": "60746948",
      "branch": "1234",
      "account": "567890",
      "holderName": "Carlos Oliveira"
    },
    "failureReason": "Destination account does not exist",
    "failureCode": "STR0010R2-AC03",
    "refundedAt": "2026-01-21T14:40:05-03:00",
    "failedAt": "2026-01-21T14:40:00-03:00"
  }
}
The failureCode field contains the SPB error code when applicable.

transfer.incoming

Sent when a TED IN transfer is received and credited.
{
  "event": "transfer.incoming",
  "timestamp": "2026-01-21T10:15:05-03:00",
  "organizationId": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "transferId": "c3d4e5f6-a7b8-9012-cdef-345678901234",
    "type": "TED_IN",
    "status": "COMPLETED",
    "controlNumber": "JD20260121000042",
    "amount": 5000.00,
    "feeAmount": 0.00,
    "netAmount": 5000.00,
    "sender": {
      "ispb": "60746948",
      "branch": "1234",
      "account": "567890",
      "holderName": "Carlos Oliveira",
      "holderDocument": "98765432100"
    },
    "recipient": {
      "accountId": "660e8400-e29b-41d4-a716-446655440001",
      "holderName": "Empresa ABC Ltda",
      "holderDocument": "12345678000199"
    },
    "creditedAt": "2026-01-21T10:15:05-03:00"
  }
}

transfer.cancelled

Sent when a transfer is cancelled by the user.
{
  "event": "transfer.cancelled",
  "timestamp": "2026-01-21T14:31:00-03:00",
  "organizationId": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "transferId": "d4e5f6a7-b8c9-0123-defg-456789012345",
    "type": "TED_OUT",
    "status": "CANCELLED",
    "amount": 1000.00,
    "feeAmount": 8.50,
    "sender": {
      "accountId": "550e8400-e29b-41d4-a716-446655440000",
      "holderName": "João Santos"
    },
    "cancellationReason": "Customer request",
    "cancelledAt": "2026-01-21T14:31:00-03:00"
  }
}

Signature validation


All webhooks include an HMAC-SHA256 signature in the X-Signature header. Validate this signature to ensure the webhook is authentic.

Signature header

X-Signature: sha256=a1b2c3d4e5f6...

Validation in code

const crypto = require('crypto');
const express = require('express');

function validateWebhook(rawBody, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const receivedSignature = signature.replace('sha256=', '');

  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature),
    Buffer.from(receivedSignature)
  );
}

// Important: Use raw body for HMAC validation, not JSON.stringify(req.body)
// JSON.stringify may produce different bytes than the original request
app.post('/webhooks/ted',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const rawBody = req.body; // Buffer containing raw request body
    const signature = req.headers['x-signature'];

    if (!validateWebhook(rawBody, signature, process.env.WEBHOOK_SECRET)) {
      return res.status(401).send('Invalid signature');
    }

    // Parse JSON after validation
    const payload = JSON.parse(rawBody.toString());

    // Process webhook...
    res.status(200).send('OK');
  }
);
import hmac
import hashlib

def validate_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    received = signature.replace('sha256=', '')

    return hmac.compare_digest(expected, received)
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "strings"
)

func validateWebhook(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := hex.EncodeToString(mac.Sum(nil))

    received := strings.TrimPrefix(signature, "sha256=")

    return hmac.Equal([]byte(expected), []byte(received))
}

Retry policy


If your endpoint doesn’t respond with 2xx status, the plugin resends the webhook with exponential backoff:
AttemptInterval
1Immediate
25 seconds
325 seconds
460 seconds
5120 seconds
After 5 unsuccessful attempts, the webhook is moved to a dead-letter queue (DLQ).

Endpoint requirements

To ensure reliable delivery:
  • Respond with 2xx status within 30 seconds
  • Use HTTPS with valid certificate
  • Avoid heavy processing in handler (use queues)

Idempotency


Webhooks may be delivered more than once in retry scenarios. Implement idempotency using the transferId:
app.post('/webhooks/ted', async (req, res) => {
  const { event, data } = req.body;
  const { transferId } = data;

  // Check if already processed this event
  const processed = await cache.get(`webhook:${transferId}:${event}`);
  if (processed) {
    return res.status(200).send('Already processed');
  }

  // Process the webhook
  await processWebhook(event, data);

  // Mark as processed (24h TTL)
  await cache.set(`webhook:${transferId}:${event}`, true, 86400);

  res.status(200).send('OK');
});

Monitoring


The plugin exposes metrics about webhook delivery:
webhook_delivery_total{event, status}
webhook_delivery_latency_ms{event}
webhook_retry_total{event, attempt}
webhook_dlq_total{event}
Use these metrics to monitor integration health and identify delivery issues.

Testing webhooks


During development, you can use tools like:
  • webhook.site — temporary endpoint for testing
  • ngrok — exposes localhost to the internet
  • RequestBin — inspects received requests
Configure the test URL in the staging environment before going to production.

Best practices


PracticeDescription
Validate signatureAlways verify the X-Signature header
Respond quicklyReturn 200 before processing; use queues
Implement idempotencySame webhook may arrive more than once
Monitor DLQSet up alerts for undelivered webhooks
Use HTTPSWebhooks are only sent to secure endpoints
Handle all eventsEven if ignoring some, return 200