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 (must be HTTPS 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) |
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 fulfilmentpayment.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 accessstanding_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
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:
- 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
- Return to Getting Started for API overview
- Follow the Quick Start guide to create payments
- View Payment API Reference for endpoint details