Webhooks
Webhooks let your systems receive real-time notifications when a payment link's payment status changes, so you don't have to poll the API. They are the push-based counterpart to the Fetch Links with Status Changes endpoint: use webhooks for immediate updates and status-changes polling for reconciliation or catch-up.
Event Types
| Event | Description |
|---|---|
payment_link.paid | The payment link has been paid in full |
payment_link.partially_paid | A partial payment has been recorded against the payment link |
How It Works
- A payment is confirmed against a payment link. Confirmations can originate from several sources:
card payment through the hosted checkout (
stripe), an alternative checkout (hippo), a recorded offline payment (offline), or automaticreconciliation. - When the payment moves the link to
paidorpartially_paid, a webhook delivery is recorded and queued for dispatch. - The dispatcher sends an HTTP
POSTrequest to the destination URL configured for your division account, signed with your division's shared secret. - Your endpoint acknowledges the delivery by returning a
2xxresponse. Non-2xxresponses, timeouts, and network errors are retried (see Retries & Delivery Guarantees).
Configuration
Webhook delivery is configured per division account during onboarding with PaySuite. Each division has:
| Setting | Description |
|---|---|
| Destination URL | The HTTPS endpoint that receives webhook POST requests |
| Shared secret | The secret used to sign each request so you can verify authenticity |
| Active flag | Delivery only occurs while the endpoint is active. When inactive, deliveries are marked skipped |
The Webhook Request
The dispatcher sends a POST request to your configured destination URL:
| Header | Description |
|---|---|
Content-Type | application/json |
X-PaymentLinks-Signature | Lowercase hex-encoded HMAC-SHA256 of the raw request body, computed with your division's shared secret |
X-PaymentLinks-Timestamp | Unix timestamp (seconds) of when the request was sent |
The request body is the full payment link object — the same structure returned by the Get Payment Link Details endpoint.
Example Payload
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"paymentLinkId": "PL-20241101103000-a1b2c3",
"divisionAccountId": "6F2A9C31-8B45-4E0B-9C77-12AF34D9B201",
"merchantId": "A91C4B87-3D0E-4B46-8E52-9BD4E57EAA98",
"customer": {
"id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
"name": "John Smith",
"email": "[email protected]"
},
"description": "Website development services Q4",
"externalReference": "INV-2024-001",
"amounts": {
"currency": "GBP",
"subtotal": 300000,
"total": 300000
},
"currencyCode": "GBP",
"statusCode": "paid",
"statusChangedAt": "2024-11-15T14:22:00Z",
"paymentLinkUrl": "https://group.pay.accessacloud.com/pay/550e8400-e29b-41d4-a716-446655440000",
"paidAmount": 300000,
"items": [
{
"id": "9b2e7c14-4f1a-4d3b-8a21-6c0f5e2d1a77",
"description": "Frontend Development",
"quantity": 40,
"unitPrice": 7500,
"amount": 300000,
"offlinePaidAmount": 0
}
],
"payments": [
{
"id": "pay_1234567890abcdef",
"amount": 300000,
"currency": "GBP",
"method": "card",
"status": "succeeded",
"date": "2024-11-15T14:22:00Z",
"externalPaymentReference": "pi_1234567890abcdef"
}
],
"createdAt": "2024-11-01T10:30:00Z",
"updatedAt": "2024-11-15T14:22:00Z",
"version": 1
}
Verifying Signatures
Always verify the signature before trusting a webhook. Recompute the HMAC-SHA256 over the exact
raw bytes of the request body using your shared secret, then compare it — using a constant-time
comparison — with the value in the X-PaymentLinks-Signature header.
using System.Security.Cryptography;
using System.Text;
static string ComputeSignature(string rawBody, string secret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
return Convert.ToHexString(hash).ToLowerInvariant();
}
// var isValid = CryptographicOperations.FixedTimeEquals(
// Encoding.UTF8.GetBytes(ComputeSignature(rawBody, secret)),
// Encoding.UTF8.GetBytes(receivedSignature));
Verify against the raw body received, not a re-serialized object, because any difference in formatting will change the signature.
Retries & Delivery Guarantees
- Your endpoint should return a
2xxstatus code to acknowledge receipt. - A delivery is retried when the destination returns
408 Request Timeout,429 Too Many Requests, any5xxstatus, or when the request times out or fails with a network error. - Other
4xxresponses are treated as permanent failures and are not retried. - Each attempt has a 30-second timeout, and a delivery is attempted up to 10 times before it is
marked
failed. - Deliveries are deduplicated: the same underlying payment confirmation is not delivered twice, even if it is reported by more than one source. Your endpoint should still be idempotent as a defensive measure.
- Successfully delivered records are retained for 90 days for audit purposes and then purged.
Delivery Statuses
| Status | Description |
|---|---|
pending | Recorded and awaiting delivery (or a retry) |
delivered | Successfully delivered (destination returned 2xx) |
failed | Delivery failed permanently (non-retryable response or retries exhausted) |
skipped | No active webhook endpoint was configured for the division |
Monitoring Webhook Deliveries
Use this endpoint to inspect outbound webhook deliveries — for example to check for pending or
failed deliveries that need attention. Only pending and failed deliveries are exposed;
delivered records are audit history and are purged automatically.
Endpoint
GET /api/v1/paymentLinks/webhook-deliveries
Header Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
x-api-key | string | required | PaySuite provided API key |
divisionAccountId | string | required | Division account identifier |
merchantId | string | required | Customer identifier as provided during customer onboarding |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string[] | optional | Filter by delivery status. Only pending and failed are supported; defaults to both. Multiple values can be provided |
since | dateTime | optional | Start of the received-at time range (ISO 8601) |
until | dateTime | optional | End of the received-at time range (ISO 8601) |
page | integer | optional | Page number (default: 1, minimum: 1) |
pageSize | integer | optional | Items per page (default: 100, maximum: 1000) |
Response Parameters
Status Code: 200 OK
| Parameter | Type | Description |
|---|---|---|
deliveries | array | Array of WebhookDeliverySummary objects |
pagination | object | Pagination information (see PaginationInfo below) |
WebhookDeliverySummary Object
| Field | Type | Description |
|---|---|---|
id | uuid | Unique identifier for the delivery |
paymentLinkId | uuid | Payment link the delivery relates to |
eventType | string | Event type (payment_link.paid, payment_link.partially_paid) |
source | string | Source of the confirmation (stripe, hippo, offline, reconciliation) |
externalEventId | string | Identifier of the originating event (nullable) |
processingStatus | string | Delivery status (pending, failed) |
receivedAt | dateTime | When the confirmation was recorded (ISO 8601) |
processedAt | dateTime | When the delivery was last processed (nullable) |
retryCount | integer | Number of retries performed so far |
nextRetryAt | dateTime | When the next retry is scheduled (nullable) |
failureReason | string | Reason for the most recent failure (nullable) |
PaginationInfo Object
| Field | Type | Description |
|---|---|---|
currentPage | integer | Current page number |
totalPages | integer | Total number of pages |
totalItems | integer | Total number of items |
itemsPerPage | integer | Items displayed per page |
hasNextPage | boolean | Whether there is a next page |
hasPreviousPage | boolean | Whether there is a previous page |
Example Request
GET /api/v1/paymentLinks/webhook-deliveries?status=failed&since=2024-11-01T00:00:00Z&page=1&pageSize=50
Example Response
{
"deliveries": [
{
"id": "e3a1b2c4-5d6f-7a8b-9c0d-1e2f3a4b5c6d",
"paymentLinkId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "payment_link.paid",
"source": "stripe",
"externalEventId": "evt_1P2x3y4z5a6b7c8d",
"processingStatus": "failed",
"receivedAt": "2024-11-15T14:22:00Z",
"processedAt": "2024-11-15T14:27:30Z",
"retryCount": 10,
"nextRetryAt": null,
"failureReason": "Destination responded with 500 (retries exhausted)"
}
],
"pagination": {
"currentPage": 1,
"totalPages": 1,
"totalItems": 1,
"itemsPerPage": 50,
"hasNextPage": false,
"hasPreviousPage": false
}
}
Status Code: 400 Bad Request
Returned when the status filter contains a value other than pending or failed.
Status Code: 401 Unauthorized
Returned when the API key is missing or invalid.