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 (HTTPS strongly recommended for production)
  4. Select which events you want to receive
  5. Save your configuration
โš ๏ธ HTTPS Recommended: Use HTTPS for production webhook URLs. HTTP can be useful for local development and controlled testing environments.

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 if enabled)

Standing Order Events

Event Description When It Fires
standing_order.created Standing order set up Customer completes Direct Debit mandate setup
standing_order.completed Standing order completed Standing order reaches its final scheduled payment
standing_order.failed Standing order failed Standing order setup fails

Standing Order Transaction Events

Event Description When It Fires
standing_order_transaction.verified Scheduled payment verified A scheduled payment is found in the connected account
standing_order_transaction.unverified Scheduled payment unverified A scheduled payment is not found after the expected date
standing_order_transaction.unmonitored Scheduled payment unmonitored Scheduled payment cannot be checked (for example, access has expired)

Pay by Bank+ Consent Events

Event Description When It Fires
pay_by_bank_plus.consent.approved Agreement approved The payer has approved the Pay by Bank+ agreement
pay_by_bank_plus.consent.failed Agreement failed The Pay by Bank+ agreement setup failed or was rejected
pay_by_bank_plus.consent.cancelled Agreement cancelled The Pay by Bank+ agreement has been cancelled

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": "COMPLETE",
  "amount": 150.00,
  "currency": "GBP",
  "description": "Product purchase",
  "reference": "ORDER-12345",
  "paymentMethod": "Pay by Bank",
  "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 at send time (commonly STARTED, COMPLETE, 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 Pay by Bank, Pay by Bank+, Card, or Direct Debit
eventType string The event that triggered this webhook

Pay by Bank+ Consent Payload

{
  "merchantId": "mch_abc123xyz",
  "orgId": "org_branch001",
  "consentId": "encrypted-consent-id",
  "created": "2026-05-28T14:30:00Z",
  "approvedUtc": "2026-05-28T14:32:00Z",
  "status": "APPROVED",
  "bankId": "ob-natwest",
  "currency": "GBP",
  "reference": "ORDER-12345",
  "description": "Pay by Bank+ agreement",
  "initialPaymentAmount": 49.88,
  "perTransactionLimit": 49.88,
  "monthlyLimit": 500.00,
  "expiresOn": null,
  "eventType": "pay_by_bank_plus.consent.approved"
}

Pay by Bank+ Consent Payload Fields

Field Type Description
consentId string Trustist Pay by Bank+ agreement identifier. Store this if you need to take later payments against the agreement.
status string Agreement status at send time, such as APPROVED, FAILED, or CANCELLED.
reference string Your reference supplied when the agreement was created.
perTransactionLimit number|null Maximum value allowed for a single Pay by Bank+ payment under this agreement.
monthlyLimit number|null Monthly cap for Pay by Bank+ payments under this agreement.
eventType string The event that triggered this webhook.

Partner-Referred Merchant Payloads

Partners that receive merchant payment events for referred merchants get Trustist-owned payloads with the same product wording. Pay by Bank+ agreement events do not expose Token.io identifiers or provider error bodies.

Partner Pay by Bank+ Payment Payload

{
  "merchantId": "mch_abc123xyz",
  "paymentId": "pmt_123456789",
  "created": "2026-05-28T14:35:00Z",
  "status": "COMPLETE",
  "amount": 49.88,
  "paymentMethod": "Pay by Bank+",
  "eventType": "PaymentStatusChanged"
}

Partner Pay by Bank+ Consent Payload

{
  "merchantId": "mch_abc123xyz",
  "consentId": "encrypted-consent-id",
  "customerId": "22222222-2222-2222-2222-222222222222",
  "created": "2026-05-28T14:30:00Z",
  "status": "APPROVED",
  "amount": 49.88,
  "currency": "GBP",
  "reference": "ORDER-12345",
  "eventType": "pay_by_bank_plus.consent.approved"
}

Partner payment webhooks use PaymentStatusChanged as the payment event type. Pay by Bank+ consent webhooks use the same consent event names as merchant webhooks: pay_by_bank_plus.consent.approved, pay_by_bank_plus.consent.failed, and pay_by_bank_plus.consent.cancelled.

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 PENDING, STARTED, PROCESSING, FAILED, ABANDONED, ACTIVE, COMPLETED
frequency string DAILY, WEEKLY, TWICEWEEKLY, MONTHLY, TWICEMONTHLY, QUARTERLY, SEMIANNUALLY, ANNUALLY
startDate string ISO date (yyyy-MM-dd) when the standing order starts
endDate string|null Optional ISO end date
numberOfPayments number|null Optional payment count limit

Implementing a Webhook Endpoint

Requirements

Your webhook endpoint must:

  • โœ… Accept HTTP POST requests
  • โœ… Return a 2xx HTTP status code quickly
  • โœ… 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.completed':
            await handleStandingOrderCompleted(payload);
            break;

        case 'standing_order.failed':
            await handleStandingOrderFailed(payload);
            break;

        case 'standing_order_transaction.verified':
        case 'standing_order_transaction.unverified':
        case 'standing_order_transaction.unmonitored':
            await handleStandingOrderTransaction(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 if retry is enabled)

Subscription Management

Use standing order webhooks to manage subscriptions:

  • standing_order.created โ†’ Activate subscription, grant access
  • standing_order.completed โ†’ Move subscription to completed/expired state
  • standing_order.failed โ†’ Mark setup as failed and prompt for a new mandate
  • standing_order_transaction.* โ†’ Track each scheduled payment outcome

Customer Communication

Trigger automated customer notifications:

  • Send confirmation emails on payment.completed
  • Send retry prompts on payment.failed (if retry is enabled for the payment)
  • 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 quickly

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 - Retried automatically

Important: Failed Payment Retries

โš ๏ธ Critical: If retry is enabled for a payment, customers can retry the same payment after a payment.failed event. 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 plain HTTP for production traffic (prefer HTTPS)
  • 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