Recaudo Docs

Events

Webhooks

Webhooks notify you in real time about events in your account. Set up an HTTPS endpoint and receive automatic updates.

Event types

Payments

  • payment.created
  • payment.succeeded
  • payment.failed
  • payment.refunded

Payment links

  • payment_link.created
  • payment_link.paid
  • payment_link.expired

Subscriptions

  • subscription.created
  • subscription.updated
  • subscription.canceled
  • subscription.paused
  • subscription.resumed
  • subscription.trial_ending

Invoices

  • invoice.created
  • invoice.paid
  • invoice.payment_failed
  • invoice.upcoming

Payload format

Webhook payload
{
  "id": "evt_1a2b3c4d5e6f",
  "object": "event",
  "type": "payment.succeeded",
  "api_version": "2024-01-01",
  "created": 1705312200,
  "data": {
    "object": {
      "id": "pi_abc123xyz",
      "object": "payment_intent",
      "amount": 500.00,
      "currency": "MXN",
      "status": "succeeded",
      "customer_id": "8e250c25-9d84-4892-98ec-d71fc1fcd3c7",
      ...
    }
  }
}

Signature verification (HMAC)

Every webhook includes a header X-Recaudo-Signature that you must verify to make sure the message comes from Recaudo.

<?php
// Obtener el payload y la firma
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_RECAUDO_SIGNATURE'];
$secret = 'whsec_tu_webhook_secret';

// Extraer timestamp y firma del header
// Formato: t=timestamp,v1=signature
$parts = explode(',', $signature);
$timestamp = substr($parts[0], 2);
$receivedSignature = substr($parts[1], 3);

// Calcular la firma esperada
$signedPayload = $timestamp . '.' . $payload;
$expectedSignature = hash_hmac('sha256', $signedPayload, $secret);

// Verificar
if (hash_equals($expectedSignature, $receivedSignature)) {
    // Firma valida - procesar el evento
    $event = json_decode($payload, true);

    switch ($event['type']) {
        case 'payment.succeeded':
// Manejar pago exitoso
break;
        case 'subscription.canceled':
// Manejar cancelacion
break;
    }

    http_response_code(200);
} else {
    // Firma invalida
    http_response_code(400);
}
const crypto = require('crypto');

app.post('/webhooks/recaudo', express.raw({type: 'application/json'}), (req, res) => {
  const payload = req.body;
  const signature = req.headers['x-recaudo-signature'];
  const secret = 'whsec_tu_webhook_secret';

  // Extraer timestamp y firma
  const parts = signature.split(',');
  const timestamp = parts[0].split('=')[1];
  const receivedSignature = parts[1].split('=')[1];

  // Calcular firma esperada
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Verificar
  if (crypto.timingSafeEqual(
    Buffer.from(expectedSignature),
    Buffer.from(receivedSignature)
  )) {
    const event = JSON.parse(payload);

    switch (event.type) {
      case 'payment.succeeded':
        // Manejar pago exitoso
        break;
      case 'subscription.canceled':
        // Manejar cancelacion
        break;
    }

    res.status(200).send('OK');
  } else {
    res.status(400).send('Invalid signature');
  }
});
import hmac
import hashlib
from flask import Flask, request

app = Flask(__name__)

@app.route('/webhooks/recaudo', methods=['POST'])
def webhook():
    payload = request.data
    signature = request.headers.get('X-Recaudo-Signature')
    secret = 'whsec_tu_webhook_secret'

    # Extraer timestamp y firma
    parts = signature.split(',')
    timestamp = parts[0].split('=')[1]
    received_signature = parts[1].split('=')[1]

    # Calcular firma esperada
    signed_payload = f"{timestamp}.{payload.decode()}"
    expected_signature = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    # Verificar
    if hmac.compare_digest(expected_signature, received_signature):
        event = request.json

        if event['type'] == 'payment.succeeded':
# Manejar pago exitoso
pass
        elif event['type'] == 'subscription.canceled':
# Manejar cancelacion
pass

        return 'OK', 200
    else:
        return 'Invalid signature', 400

Best practices

  • Always verify the HMAC signature before processing the event
  • Respond with 200 quickly and process asynchronously
  • Implement idempotency using the event ID
  • Use HTTPS with a valid certificate
  • Webhooks are retried automatically if they fail (up to 3 times)

Webhook Endpoints API

Manage your webhook endpoints programmatically.

POST /v1/webhook-endpoints

Create a new webhook endpoint.

Request body
{
  "url": "https://tu-servidor.com/webhooks/recaudo",
  "events": ["payment.succeeded", "invoice.paid", "customer.created"],
  "description": "Webhook principal de producción",
  "is_active": true
}
GET /v1/webhook-endpoints

List all configured webhook endpoints.

GET /v1/webhook-endpoints/{'{id}'}

View the endpoint details including the secret to verify signatures.

Response
{
  "id": "we_abc123",
  "url": "https://tu-servidor.com/webhooks/recaudo",
  "events": ["payment.succeeded", "invoice.paid"],
  "secret": "whsec_tu_webhook_secret",
  "is_active": true,
  "description": "Webhook principal",
  "created_at": "2026-04-01T00:00:00Z"
}
PUT /v1/webhook-endpoints/{'{id}'}

Update the endpoint URL, events or status.

Request body
{
  "url": "https://nueva-url.com/webhooks",
  "events": ["payment.succeeded", "payment.failed"],
  "is_active": false
}
DELETE /v1/webhook-endpoints/{'{id}'}

Delete a webhook endpoint.

POST /v1/webhook-endpoints/{'{id}'}/test

Send a test event to the endpoint to check connectivity.

POST /v1/webhook-endpoints/{'{id}'}/rotate-secret

Generate a new secret for the endpoint. The previous secret stops working immediately.

GET /v1/webhook-endpoints/{'{id}'}/deliveries

View the endpoint delivery history with status and response codes.

POST /v1/webhook-endpoints/{'{id}'}/deliveries/{'{delivery_id}'}/resend

Resend a failed delivery.

Was this page helpful? Thanks for letting us know.
Esc