> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rinne.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time event notifications from Rinne

Webhooks allow you to receive real-time notifications when events occur in your Rinne account. Instead of polling the API, Rinne sends HTTP POST requests to your server when important events happen.

## Webhook events

Rinne sends webhooks for the following events:

### Transaction events

* `transaction.created`: New transaction created
* `transaction.status-changed`: Transaction status updated

### Affiliation events

* `affiliation.created`: New affiliation created
* `affiliation.status-changed`: Affiliation status updated

### Company events

* `company.created`: New company created
* `company.status-changed`: Company status updated

### Banking events

* `cashout.created`: New cashout request created
* `cashout.status-changed`: Cashout status updated
* `internal-transfer.created`: New internal transfer created
* `internal-transfer.status-changed`: Internal transfer status updated

## Webhook payload structure

All webhooks follow a consistent structure:

```json theme={null}
{
  "id": "evt_123456789",
  "type": "transaction.status-changed",
  "version": 1,
  "timestamp": "2025-01-21T10:30:00.000Z",
  "source": "transaction-service",
  "company_id": "company-123",
  "correlation_id": "corr-987654321",
  "payload": {
    "transaction_id": "tx_123456789",
    "old_status": "WAITING_PAYMENT",
    "new_status": "APPROVED"
  }
}
```

### Common fields

* `id`: Unique event identifier
* `type`: Event type (e.g., `transaction.status-changed`)
* `version`: Event schema version
* `timestamp`: When the event occurred (ISO 8601)
* `source`: Service that generated the event
* `company_id`: Company associated with the event
* `correlation_id`: ID for tracking related events
* `payload`: Event-specific data

## Setting up webhooks

Contact Rinne support to configure your webhook endpoint URL. We'll create a webhook configuration for your organization.

### Webhook endpoint requirements

Your endpoint must:

* Accept POST requests
* Respond with 2xx status code within 5 seconds
* Use HTTPS (required for production)
* Handle duplicate events (idempotency)

## Webhook security

Rinne uses Svix for webhook delivery, which provides:

* Request signing for verification
* Automatic retries with exponential backoff
* Webhook dashboard for monitoring
* Replay functionality for testing

### Verifying webhook signatures

Verify that webhooks are from Rinne by checking the signature:

```javascript theme={null}
const svix = require('svix');

app.post('/webhooks/rinne', (req, res) => {
  const payload = req.body;
  const headers = req.headers;
  
  const wh = new svix.Webhook(WEBHOOK_SECRET);
  
  try {
    const verified = wh.verify(payload, headers);
    // Process the webhook
    res.status(200).send('OK');
  } catch (err) {
    res.status(400).send('Invalid signature');
  }
});
```

## Handling webhook events

### Transaction status changed

```javascript theme={null}
if (event.type === 'transaction.status-changed') {
  const { transaction_id, old_status, new_status, refund_id } = event.payload;
  
  if (new_status === 'APPROVED') {
    // Payment approved - fulfill order
    await fulfillOrder(transaction_id);
  } else if (new_status === 'REFUSED') {
    // Payment declined - notify customer
    await notifyPaymentFailed(transaction_id);
  } else if (new_status === 'PENDING_REFUND') {
    // Refund initiated - notify customer
    await notifyRefundInitiated(transaction_id);
  } else if (new_status === 'REFUNDED' || new_status === 'PARTIALLY_REFUNDED') {
    // Refund completed - update order status
    await updateOrderRefundStatus(transaction_id, new_status);
  } else if (old_status === 'PENDING_REFUND' && new_status === 'APPROVED') {
    // Refund failed - transaction returned to approved
    await notifyRefundFailed(transaction_id);
  }
}
```

#### Refund failure handling

When a refund fails, you'll receive a `transaction.status-changed` webhook where:

* `old_status` is `PENDING_REFUND`
* `new_status` is `APPROVED` (if no other refunds exist) or remains `PENDING_REFUND` (if other refunds are still processing)

This allows you to handle refund failures appropriately in your system and notify customers when a refund could not be processed.

### Affiliation status changed

```javascript theme={null}
if (event.type === 'affiliation.status-changed') {
  const { affiliation_id, new_status, onboarding_url, provider_status_message } = event.payload;
  
  if (new_status === 'ACTIVE') {
    // Affiliation activated - merchant can process transactions
    await enableMerchantPayments(affiliation_id);
  } else if (new_status === 'WAITING_DOCUMENTS') {
    // Provider needs documents - send onboarding URL to merchant
    await sendOnboardingLink(affiliation_id, onboarding_url);
  } else if (new_status === 'BLOCKED' && provider_status_message) {
    // Affiliation blocked - log provider message for debugging
    await logAffiliationBlocked(affiliation_id, provider_status_message);
  } else if (new_status === 'INVALID_SETTLEMENT_BANK_ACCOUNT') {
    // Settlement bank account rejected by provider — notify and prompt a replacement
    await notifyInvalidSettlementBank(affiliation_id, provider_status_message);
  }
}
```

<Note>
  The `provider_status_message` field contains additional context from the payment provider when available, including possible error messages.
</Note>

### Cashout created

```javascript theme={null}
if (event.type === 'cashout.created') {
  const { cashout_id, amount, currency, status } = event.payload;
  
  // Cashout request created
  await logCashoutCreated(cashout_id, amount, currency);
}
```

### Cashout status changed

```javascript theme={null}
if (event.type === 'cashout.status-changed') {
  const { cashout_id, old_status, new_status, error_message } = event.payload;
  
  if (new_status === 'COMPLETED') {
    // Cashout completed successfully
    await notifyCashoutCompleted(cashout_id);
  } else if (new_status === 'PARTIALLY_RETURNED') {
    // Part of the cashout was returned
    const cashout = await getCashoutDetails(cashout_id);
    await notifyPartialReturn(cashout_id, cashout.returned_amount);
  } else if (new_status === 'RETURNED') {
    // Full cashout amount was returned
    await notifyFullReturn(cashout_id);
  } else if (new_status === 'FAILED') {
    // Cashout failed - error_message contains details when available
    await notifyCashoutFailed(cashout_id, error_message);
  }
}
```

### Internal transfer status changed

```javascript theme={null}
if (event.type === 'internal-transfer.status-changed') {
  const { internal_transfer_id, old_status, new_status, error_message } = event.payload;
  
  if (new_status === 'COMPLETED') {
    // Transfer completed successfully
    await notifyTransferCompleted(internal_transfer_id);
  } else if (new_status === 'FAILED') {
    // Transfer failed - error_message contains details when available
    await notifyTransferFailed(internal_transfer_id, error_message);
  }
}
```

## Webhook dashboard

Access your webhook dashboard to:

* View webhook delivery history
* Replay failed webhooks
* Test webhook endpoints
* Monitor delivery success rates

Get your dashboard URL:

```bash theme={null}
curl https://api-sandbox.rinne.com.br/core/v1/webhooks/dashboard \
  -H "x-api-key: YOUR_API_KEY"
```

## Retry behavior

If your endpoint returns an error or times out, Rinne automatically retries:

* Immediate retry
* After 5 seconds
* After 5 minutes
* After 30 minutes
* After 2 hours
* After 5 hours
* After 10 hours
* After 24 hours

After all retries fail, the webhook is marked as failed in your dashboard.

## Best practices

<AccordionGroup>
  <Accordion title="Respond quickly">
    Return a 200 status code immediately, then process the webhook asynchronously. Don't perform long-running operations in the webhook handler.
  </Accordion>

  <Accordion title="Handle duplicates">
    Use the event `id` to track processed events and ignore duplicates. Webhooks may be delivered more than once.
  </Accordion>

  <Accordion title="Verify signatures">
    Always verify webhook signatures to ensure requests are from Rinne.
  </Accordion>

  <Accordion title="Use correlation IDs">
    Use `correlation_id` to track related events across different webhook types.
  </Accordion>

  <Accordion title="Monitor webhook health">
    Regularly check your webhook dashboard for failed deliveries and fix issues promptly.
  </Accordion>
</AccordionGroup>

## Testing webhooks

### Local testing with ngrok

Use ngrok to expose your local server for webhook testing:

```bash theme={null}
ngrok http 3000
```

Provide the ngrok URL to Rinne support for webhook configuration.

### Webhook replay

Use the webhook dashboard to replay events to your endpoint for testing.

## Example webhook handler

```javascript theme={null}
const express = require('express');
const { Webhook } = require('svix');

const app = express();
app.use(express.json());

app.post('/webhooks/rinne', async (req, res) => {
  const payload = JSON.stringify(req.body);
  const headers = req.headers;
  
  const wh = new Webhook(process.env.WEBHOOK_SECRET);
  
  let event;
  try {
    event = wh.verify(payload, headers);
  } catch (err) {
    return res.status(400).send('Invalid signature');
  }
  
  // Respond immediately
  res.status(200).send('OK');
  
  // Process asynchronously
  processWebhook(event).catch(console.error);
});

async function processWebhook(event) {
  switch (event.type) {
    case 'transaction.status-changed':
      await handleTransactionStatusChanged(event.payload);
      break;
    case 'affiliation.status-changed':
      await handleAffiliationStatusChanged(event.payload);
      break;
    // Handle other event types
  }
}

app.listen(3000);
```

## Next steps

<CardGroup cols={2}>
  <Card title="API Reference" icon="code" href="/api-reference/introduction">
    Explore all webhook event schemas
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation" href="/guides/error-handling">
    Handle errors and edge cases
  </Card>
</CardGroup>
