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.
Setting Up Payment Webhooks
Payment webhooks are configured through the TrustistTransfer portal, not via the TE API:
- Log in to TrustistTransfer
- Navigate to Settings โ API Keys
- Enter your webhook endpoint URL (HTTPS strongly recommended for production)
- Select which events you want to receive
- Save your configuration
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 fulfilmentpayment.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 accessstanding_order.completedโ Move subscription to completed/expired statestanding_order.failedโ Mark setup as failed and prompt for a new mandatestanding_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
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:
- Navigate to Settings โ API Keys
- View webhook delivery history
- Check HTTP status codes and error messages
- 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
- Read Payment Attempts for bank/card execution flows
- Return to Getting Started for API overview
- Follow the Quick Start guide to create payments
- View Payment API Reference for endpoint details