Payment Webhooks

Receive real-time notifications when payment events occur in your Trustist merchant account. Payment webhooks enable you to automate order fulfilment, update customer records, and trigger business workflows instantly.

๐Ÿ“Œ Note: Payment webhooks are configured per merchant in the TrustistTransfer portal. They are separate from tenant webhooks (used for identity verification session notifications).

Setting Up Payment Webhooks

Payment webhooks are configured through the TrustistTransfer portal, not via the TE API:

  1. Log in to TrustistTransfer
  2. Navigate to Settings โ†’ API Keys
  3. Enter your webhook endpoint URL (must be HTTPS for production)
  4. Select which events you want to receive
  5. Save your configuration
โš ๏ธ HTTPS Required: Production webhook URLs must use HTTPS. HTTP is only allowed for local development and testing.

Available Events

Payment Events

Event Description When It Fires
payment.created Payment initiated Customer starts a payment via hosted page or API
payment.completed Payment successful Payment successfully processed, funds will be transferred
payment.failed Payment failed Payment attempt failed (customer can retry)

Standing Order Events

Event Description When It Fires
standing_order.created Standing order set up Customer completes Direct Debit mandate setup
standing_order.updated Standing order modified Existing standing order is changed
standing_order.cancelled Standing order cancelled Standing order is cancelled by customer or merchant

Webhook Payload Structure

All payment webhooks are sent as HTTP POST requests with a JSON payload.

Payment Event Payload

{
  "merchantId": "mch_abc123xyz",
  "paymentId": "pmt_123456789",
  "orgId": "org_branch001",
  "created": "2025-10-21T14:30:00Z",
  "status": "COMPLETED",
  "amount": 150.00,
  "currency": "GBP",
  "description": "Product purchase",
  "reference": "ORDER-12345",
  "paymentMethod": "Bank Transfer",
  "eventType": "payment.completed"
}

Payment Payload Fields

Field Type Description
merchantId string Your merchant identifier
paymentId string Unique payment identifier
orgId string Organization/branch ID (if provided during payment creation)
created string ISO 8601 timestamp when payment was created
status string Payment status: CREATED, COMPLETED, FAILED
amount number Payment amount (decimal)
currency string ISO currency code (e.g., GBP, EUR)
description string Payment description
reference string Your reference/order number
paymentMethod string Bank Transfer, Card, or Direct Debit
eventType string The event that triggered this webhook

Standing Order Event Payload

{
  "merchantId": "mch_abc123xyz",
  "standingOrderId": "so_123456789",
  "orgId": "org_branch001",
  "created": "2025-10-21T14:30:00Z",
  "status": "ACTIVE",
  "amount": 50.00,
  "currency": "GBP",
  "frequency": "MONTHLY",
  "description": "Monthly subscription",
  "reference": "SUB-12345",
  "eventType": "standing_order.created"
}

Standing Order Payload Fields

Field Type Description
standingOrderId string Unique standing order identifier
status string ACTIVE, CANCELLED, SUSPENDED
frequency string WEEKLY, FORTNIGHTLY, MONTHLY, QUARTERLY, ANNUALLY

Implementing a Webhook Endpoint

Requirements

Your webhook endpoint must:

  • โœ… Accept HTTP POST requests
  • โœ… Return a 2xx HTTP status code within 30 seconds
  • โœ… Be publicly accessible (not localhost or private network)
  • โœ… Use HTTPS in production
  • โœ… Process events asynchronously to avoid timeouts
  • โœ… Handle duplicate events idempotently

Example Implementation (Node.js/Express)

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

app.use(express.json());

app.post('/webhooks/trustist/payments', async (req, res) => {
    // Immediately acknowledge receipt
    res.status(200).send('OK');
    
    // Process asynchronously
    processPaymentWebhookAsync(req.body).catch(err => {
        console.error('Webhook processing failed:', err);
    });
});

async function processPaymentWebhookAsync(payload) {
    const { eventType, paymentId, status, amount, reference } = payload;
    
    console.log(`Processing webhook: ${eventType} for payment ${paymentId}`);
    
    // Check for duplicates using paymentId + eventType
    const existing = await db.webhookEvents.findOne({ 
        paymentId, 
        eventType 
    });
    
    if (existing) {
        console.log('Duplicate webhook, skipping');
        return;
    }
    
    // Log the event
    await db.webhookEvents.create({ 
        paymentId, 
        eventType, 
        processedAt: new Date(),
        payload 
    });
    
    // Handle the event
    switch (eventType) {
        case 'payment.created':
            await handlePaymentCreated(payload);
            break;
            
        case 'payment.completed':
            await handlePaymentCompleted(payload);
            break;
            
        case 'payment.failed':
            await handlePaymentFailed(payload);
            break;
            
        case 'standing_order.created':
            await handleStandingOrderCreated(payload);
            break;
            
        case 'standing_order.cancelled':
            await handleStandingOrderCancelled(payload);
            break;
            
        default:
            console.warn(`Unknown event type: ${eventType}`);
    }
}

async function handlePaymentCompleted(payload) {
    const { reference, amount, paymentId } = payload;
    
    // Update order status
    await db.orders.update(
        { orderNumber: reference },
        { 
            status: 'PAID',
            paidAt: new Date(),
            paymentId: paymentId,
            paidAmount: amount
        }
    );
    
    // Trigger fulfilment
    await fulfillmentService.processOrder(reference);
    
    // Send confirmation email
    await emailService.sendPaymentConfirmation(reference);
}

Example Implementation (C# ASP.NET Core)

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("webhooks/trustist/payments")]
public class TrustistPaymentWebhookController : ControllerBase
{
    private readonly ILogger _logger;
    private readonly IBackgroundTaskQueue _taskQueue;
    
    public TrustistPaymentWebhookController(
        ILogger logger,
        IBackgroundTaskQueue taskQueue)
    {
        _logger = logger;
        _taskQueue = taskQueue;
    }
    
    [HttpPost]
    public IActionResult HandleWebhook([FromBody] PaymentWebhookPayload payload)
    {
        // Queue for background processing
        _taskQueue.QueueBackgroundWorkItem(async token =>
        {
            await ProcessWebhookAsync(payload, token);
        });
        
        // Immediately return success
        return Ok();
    }
    
    private async Task ProcessWebhookAsync(
        PaymentWebhookPayload payload,
        CancellationToken ct)
    {
        _logger.LogInformation("Processing webhook: {Event} for payment {PaymentId}", 
            payload.EventType, payload.PaymentId);
        
        // Check for duplicates
        var exists = await _db.WebhookEvents
            .AnyAsync(e => e.PaymentId == payload.PaymentId && 
                          e.EventType == payload.EventType, ct);
        
        if (exists)
        {
            _logger.LogInformation("Duplicate webhook, skipping");
            return;
        }
        
        // Log the event
        await _db.WebhookEvents.AddAsync(new WebhookEvent
        {
            PaymentId = payload.PaymentId,
            EventType = payload.EventType,
            ProcessedAt = DateTime.UtcNow,
            Payload = JsonSerializer.Serialize(payload)
        }, ct);
        
        await _db.SaveChangesAsync(ct);
        
        // Handle the event
        switch (payload.EventType)
        {
            case "payment.completed":
                await HandlePaymentCompletedAsync(payload, ct);
                break;
                
            case "payment.failed":
                await HandlePaymentFailedAsync(payload, ct);
                break;
                
            // ... other cases
        }
    }
}

Common Integration Patterns

Order Status Updates

Use payment webhooks to automatically update order statuses:

  • payment.created โ†’ Set order to "Payment Pending"
  • payment.completed โ†’ Set order to "Paid", trigger fulfilment
  • payment.failed โ†’ Set order to "Payment Failed" (keep available for retry)

Subscription Management

Use standing order webhooks to manage subscriptions:

  • standing_order.created โ†’ Activate subscription, grant access
  • standing_order.cancelled โ†’ Cancel subscription, revoke access

Customer Communication

Trigger automated customer notifications:

  • Send confirmation emails on payment.completed
  • Send retry prompts on payment.failed (if retry suppression is not enabled)
  • Send subscription confirmation on standing_order.created

Webhook Delivery & Retry Policy

Success Criteria

Trustist considers a webhook successfully delivered if your endpoint:

  • Returns an HTTP 2xx status code (200, 201, 204, etc.)
  • Responds within 30 seconds

Retry Policy

If webhook delivery fails, Trustist automatically retries:

  • Attempt 1: Immediate
  • Attempt 2: After 1 second
  • Attempt 3: After 3 seconds
  • Attempt 4: After 10 seconds

Failure Handling

  • 4xx responses (client errors) - Not retried (fix your endpoint)
  • 5xx responses (server errors) - Retried automatically
  • Network failures - Retried automatically
  • Timeouts (>30s) - Retried automatically

Important: Failed Payment Retries

โš ๏ธ Critical: When a payment fails (payment.failed), customers can retry the same payment. This means a failed payment may later become completed.

Do not permanently cancel orders on payment.failed events. Keep them available for retry and only cancel after a timeout period or explicit user action.

Security Best Practices

โœ… Do's
  • Use HTTPS for all production webhook URLs
  • Validate webhook payload structure and required fields
  • Implement idempotency using paymentId + eventType
  • Log all webhook receipts for audit and debugging
  • Respond quickly (under 10 seconds ideally)
  • Queue heavy processing for background workers
  • Monitor webhook delivery success rates
  • Consider IP whitelisting for webhook endpoints
โŒ Don'ts
  • Don't perform long-running operations in the webhook handler
  • Don't permanently cancel orders on payment.failed
  • Don't assume webhooks arrive in order
  • Don't assume you'll only receive each webhook once
  • Don't use HTTP in production (HTTPS required)
  • Don't hardcode expected values without validation

Testing Webhooks

Local Development with ngrok

# Install ngrok
npm install -g ngrok

# Start your local server (e.g., port 3000)
node server.js

# Create tunnel in another terminal
ngrok http 3000

# Use the ngrok HTTPS URL in TrustistTransfer settings
# Example: https://abc123.ngrok.io/webhooks/trustist/payments

Webhook Testing Tools

  • webhook.site - Get instant webhook URL, inspect payloads
  • RequestBin - Collect and inspect HTTP requests
  • Hookdeck - Advanced webhook testing with retry and routing

Troubleshooting

Webhooks Not Received

Check:

  • Webhook URL is correct and publicly accessible
  • Firewall allows incoming connections from Trustist
  • SSL certificate is valid (for HTTPS URLs)
  • Server is responding with 2xx status codes
  • Check webhook delivery logs in TrustistTransfer portal

Duplicate Webhooks

Receiving the same webhook multiple times is expected behavior due to retries. Your handler should be idempotent:

// Check if already processed
const existing = await db.processedWebhooks.findOne({ 
    paymentId: payload.paymentId,
    eventType: payload.eventType 
});

if (existing) {
    console.log('Already processed, skipping');
    return;
}

// Process the webhook
await processPayment(payload);

// Mark as processed
await db.processedWebhooks.create({ 
    paymentId: payload.paymentId,
    eventType: payload.eventType,
    processedAt: new Date()
});

Failed Deliveries

View failed webhook deliveries in the TrustistTransfer portal:

  1. Navigate to Settings โ†’ API Keys
  2. View webhook delivery history
  3. Check HTTP status codes and error messages
  4. Manually retry failed webhooks if needed

Monitoring & Observability

Best practices for production webhook monitoring:

  • Log all webhooks - Keep a record of every webhook received
  • Alert on failures - Set up alerts if webhook processing fails
  • Track processing time - Monitor how long webhooks take to process
  • Monitor duplicate rate - Track how often you receive duplicates
  • Check delivery lag - Monitor time between event and webhook receipt

Next Steps