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:
{
"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:
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
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
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 );
}
}
The provider_status_message field contains additional context from the payment provider when available, including possible error messages.
Cashout created
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
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
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:
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
Return a 200 status code immediately, then process the webhook asynchronously. Don’t perform long-running operations in the webhook handler.
Use the event id to track processed events and ignore duplicates. Webhooks may be delivered more than once.
Always verify webhook signatures to ensure requests are from Rinne.
Use correlation_id to track related events across different webhook types.
Regularly check your webhook dashboard for failed deliveries and fix issues promptly.
Testing webhooks
Local testing with ngrok
Use ngrok to expose your local server for webhook testing:
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
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