Skip to main content

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​

EventDescription
payment_link.paidThe payment link has been paid in full
payment_link.partially_paidA partial payment has been recorded against the payment link

How It Works​

  1. 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 automatic reconciliation.
  2. When the payment moves the link to paid or partially_paid, a webhook delivery is recorded and queued for dispatch.
  3. The dispatcher sends an HTTP POST request to the destination URL configured for your division account, signed with your division's shared secret.
  4. Your endpoint acknowledges the delivery by returning a 2xx response. Non-2xx responses, 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:

SettingDescription
Destination URLThe HTTPS endpoint that receives webhook POST requests
Shared secretThe secret used to sign each request so you can verify authenticity
Active flagDelivery 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:

HeaderDescription
Content-Typeapplication/json
X-PaymentLinks-SignatureLowercase hex-encoded HMAC-SHA256 of the raw request body, computed with your division's shared secret
X-PaymentLinks-TimestampUnix 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 2xx status code to acknowledge receipt.
  • A delivery is retried when the destination returns 408 Request Timeout, 429 Too Many Requests, any 5xx status, or when the request times out or fails with a network error.
  • Other 4xx responses 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​

StatusDescription
pendingRecorded and awaiting delivery (or a retry)
deliveredSuccessfully delivered (destination returned 2xx)
failedDelivery failed permanently (non-retryable response or retries exhausted)
skippedNo 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​

ParameterTypeRequiredDescription
x-api-keystringrequiredPaySuite provided API key
divisionAccountIdstringrequiredDivision account identifier
merchantIdstringrequiredCustomer identifier as provided during customer onboarding

Query Parameters​

ParameterTypeRequiredDescription
statusstring[]optionalFilter by delivery status. Only pending and failed are supported; defaults to both. Multiple values can be provided
sincedateTimeoptionalStart of the received-at time range (ISO 8601)
untildateTimeoptionalEnd of the received-at time range (ISO 8601)
pageintegeroptionalPage number (default: 1, minimum: 1)
pageSizeintegeroptionalItems per page (default: 100, maximum: 1000)

Response Parameters​

Status Code: 200 OK​

ParameterTypeDescription
deliveriesarrayArray of WebhookDeliverySummary objects
paginationobjectPagination information (see PaginationInfo below)
WebhookDeliverySummary Object​
FieldTypeDescription
iduuidUnique identifier for the delivery
paymentLinkIduuidPayment link the delivery relates to
eventTypestringEvent type (payment_link.paid, payment_link.partially_paid)
sourcestringSource of the confirmation (stripe, hippo, offline, reconciliation)
externalEventIdstringIdentifier of the originating event (nullable)
processingStatusstringDelivery status (pending, failed)
receivedAtdateTimeWhen the confirmation was recorded (ISO 8601)
processedAtdateTimeWhen the delivery was last processed (nullable)
retryCountintegerNumber of retries performed so far
nextRetryAtdateTimeWhen the next retry is scheduled (nullable)
failureReasonstringReason for the most recent failure (nullable)
PaginationInfo Object​
FieldTypeDescription
currentPageintegerCurrent page number
totalPagesintegerTotal number of pages
totalItemsintegerTotal number of items
itemsPerPageintegerItems displayed per page
hasNextPagebooleanWhether there is a next page
hasPreviousPagebooleanWhether 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.