Show / Hide Table of Contents

WebHooks

Webhooks are server to server notifications. You give PayLolly the urls in the webHooks object of the payment request (hosted, async or sync) and PayLolly calls them as the payment progresses.

Field Triggered Body
webHooks.completed whenever the payment reaches a final state (approved, declined or error). Not sent while the payment is still pending (for example during 3D Secure) the payment status, see Payment webhooks
webHooks.approved only when the payment is approved the payment status
webHooks.txUpdate whenever a transaction of the payment changes (capture, void, refund...) the transaction status, see Transaction update webhook
Note

If both set, and transaction is approved, both hooks will be invoked

Note

For a hosted payment the same webHooks object applies to the whole flow: the cardholder's form, 3D Secure and the return to your site. You get one notification per event, not one per page.

Delivery

  • Every webhook is an HTTP POST with a JSON body (content-type: application/json).
  • Success is any 2xx response. Anything else (another status, a timeout or a connection error) is a failure and is retried.
  • PayLolly waits up to 60 seconds for your answer. Your endpoint should answer much faster (see the notes below): do the work after answering.
  • Retries: up to 26 attempts: the first retry after 1 minute, then after 5 minutes, then after 10 minutes, then hourly, which is about a day in all. After the last attempt the notification is dropped, so reconcile with GetPaymentStatus for payments you never heard about.
  • The JSON uses camel case names, properties without a value are left out (they are not sent as null), and enumerations are sent as their names (for example "status": "Approved").
Warning

Once Invoked, system will keep trying for a period of about 24 hours to send the webrequest and awaits a 2xx response

Important Notes

  1. Please note webhooks are called from a different set of IP addresses, and being webhooks, you must not assume anything
  2. Additionally please understand that webhooks will NOT contain any session data or variables
  3. Webhooks should be coded to be called multiple times without failure and should not take longer than 10 seconds to process. Use paymentId (and, for transactions, the transaction id) to recognise a repeat
  4. Webhooks can arrive in a different order from the one the events happened in (a retry of an earlier one may arrive after a later one): use the content, not the arrival order
  5. Webhooks should create Traces in your application to allow you to understand execution, flow and status
  6. Webhooks are not possible to be tested on localhost, and you should use a proxy such as ngrok or a remote webhook tester such as https://webhook.site to test

Validation

Webhook calls can be validated using 2 methods

  1. IP FILTER - in production all webhook calls originate from webhooksource.paylolly.app
  2. Webhook calls contain a header called sha512, which contains the Sha512 of the Body, contcatenated with the merchant private key (API 1.7.10 and above)

The header value is the upper case hexadecimal SHA-512 of the exact body text (UTF-8) followed directly by your merchant key (the MerchantKey you use in your API calls, written as a guid with dashes in lower case). Compute it over the raw body as received, before parsing it, and compare in a constant time.

using System.Security.Cryptography;
using System.Text;

static bool IsValid(string rawBody, string merchantKey, string sha512Header)
{
    var hash = SHA512.HashData(Encoding.UTF8.GetBytes(rawBody + merchantKey));
    return CryptographicOperations.FixedTimeEquals(
        Encoding.ASCII.GetBytes(Convert.ToHexString(hash)),
        Encoding.ASCII.GetBytes(sha512Header.ToUpperInvariant()));
}

Sha512 example

content:

{"paymentId":"5f723143-5cc4-4462-b469-cd515c6e8418","merchantId":"dbdab964-82ee-4b09-a26b-68403c31afa7","currency":"EUR","amount":1.0,"result":{"status":"Error","message":"HPS02 payment cancelled"},"merchantRef":"4a72fa2a-e5f5-422f-885d-9fdb40c1ba26"}35236fcf-12f4-4c94-b4ba-5d236457b68a

hash:

E82B614E68B14389C893F61BF54F3FA4E57629B2ABE1BEC9CEFD994B79633EA0C122ACB69299C47D79DDC18B319C06356331AAD42033D0284F06A6BAD791B6D8

Payment webhooks (completed and approved)

Important

Webhooks are all POST

The body is the same as the response of Payment/GetPaymentStatus:

Field Description
paymentId, merchantId, accountId The payment, your merchant and the merchant account used
currency, amount As requested
merchantRef Your reference
cardData maskedPAN, expiryYYMM, brand, type, country, and the card token when the card was saved (recurrence OneClick)
threeDSData The 3D Secure authentication data, when the payment was authenticated
result status (Approved, Declined, Error), message, processorId, authCode, traceId, standardHostCode, mastercardMacCode
agreement Only when the request carried an agreement object, see below
remarks Free text from PayLolly, when there is one

headers:

connection	close
content-length	309
content-type	application/json; charset=utf-8
accept-encoding	gzip, deflate, br
user-agent	RestSharp/108.0.2.0
accept	application/json, text/json, text/x-json, text/javascript, application/xml, text/xml
host	webhook.site
sha512	E82B614E68B14389C893F61BF54F3FA4E57629B2ABE1BEC9CEFD994B79633EA0C122ACB69299C47D79DDC18B319C06356331AAD42033D0284F06A6BAD791B6D8

Sample webhook call - error

{
  "paymentId": "8c08a2c5-7e9c-4a28-a29f-58d2a2cb3a2e",
  "merchantId": "dbdab964-82ee-4b09-a26b-68403c31afa7",
  "currency": "EUR",
  "amount": 1,
  "cardData": {
    "maskedPAN": "400000*0002",
    "expiryYYMM": "3012"
  },
  "result": {
    "status": "Error",
    "message": "HPS02 payment cancelled"
  },
  "merchantRef": "9e31818e-407b-48d8-8bbe-eb19fa565734"
}

Sample webhook call - declined

{
  "paymentId": "0198ab5c-1c10-7a33-8c0e-9d3f6d1b6e21",
  "merchantId": "dbdab964-82ee-4b09-a26b-68403c31afa7",
  "accountId": "81de44cb-4b70-4207-9f5e-54355491a5cc",
  "currency": "EUR",
  "amount": 1,
  "cardData": {
    "maskedPAN": "400000*0002",
    "expiryYYMM": "3012",
    "brand": "Visa",
    "type": "Credit",
    "country": "GB"
  },
  "result": {
    "status": "Declined",
    "processorId": "45f34caa-5064-4476-bb94-d8be0a500251",
    "message": "Do not honour",
    "standardHostCode": "05"
  },
  "merchantRef": "9e31818e-407b-48d8-8bbe-eb19fa565722"
}

Sample webhook call - approved

{
  "paymentId": "597ed30f-9510-4990-ad07-73135ba8fcb3",
  "merchantId": "dbdab964-82ee-4b09-a26b-68403c31afa7",
  "accountId": "81de44cb-4b70-4207-9f5e-54355491a5cc",
  "currency": "EUR",
  "amount": 1,
  "cardData": {
    "maskedPAN": "400000*0002",
    "expiryYYMM": "3012",
    "token": "9C2C6412351A5BED6529ED5698DE395E",
    "brand": "Visa",
    "type": "Credit",
    "country": "GB"
  },
  "threeDSData": {
    "cavv": "AAcBAFAUUgAAADTOl4FpdQAAAAA=",
    "eci": "5",
    "version": "2.2.0",
    "authenticationType": "02",
    "transStatus": "Y",
    "dsId": "97f8af6f-7b7c-4084-95f1-c6560fe75221",
    "acsId": "ecfd38fc-0d9f-47b1-a61e-f9767958ae65"
  },
  "result": {
    "status": "Approved",
    "processorId": "45f34caa-5064-4476-bb94-d8be0a500251",
    "message": "Dummy success",
    "authCode": "123456"
  },
  "merchantRef": "9e31818e-407b-48d8-8bbe-eb19fa565721"
}

The agreement block

When the request carried an agreement object (see Hosted Payment) the payment webhooks carry the outcome of the registration:

"agreement": {
  "agreementId": "0198ab5c-6f37-7a02-9d7e-3a5e2c1f4b10",
  "status": "Created",
  "agreementRef": "TOPUP-1",
  "consentStatus": "Recorded"
}
Field Values
agreementId The id to quote as agreementId on later payments. Absent when nothing was registered
status Created, Updated (the same customer, card and agreementRef again) or NotCreated
reason Only with NotCreated: ConsentDeclined (optional consent not ticked), AlreadyRegistered (hosted, optional consent: the saved card already has an agreement with these terms), PaymentNotApproved, Disabled (agreements are not enabled for the merchant) or Error
agreementRef Your agreementRef, as sent
consentStatus Recorded, or LegacyUnrecorded when no consent evidence is held for the agreement
Note

The agreement is registered after the payment is approved, and a failure to register never fails the payment. With "reason": "Error" PayLolly keeps retrying the registration in the background. When a retry gives the final outcome the payment is updated (GetPaymentStatus shows it) and your completed and approved webhooks are sent again with the final agreement block, so be ready to receive a second notification for the same payment (webhooks must be safe to repeat anyway).

Note

Payments without an agreement object have no agreement property.

Transaction update webhook (txUpdate)

Sent to webHooks.txUpdate whenever a transaction of the payment changes. The body is the transaction status:

Field Description
id The transaction id
paymentId The payment the transaction belongs to
merchantId, accountId Your merchant and the merchant account
currency The currency
authAmount, captureAmount, refundAmount Amounts authorised, captured and refunded so far
state Decline, Error, Auth, Capture, Void, PartialRefund, Refund or Oct
finalCapture true when the capture was the final one
created When the transaction was created (UTC)
{
  "id": "6a0b6c58-0a3c-4c0e-bb3b-1f8f0c0c7a11",
  "merchantId": "dbdab964-82ee-4b09-a26b-68403c31afa7",
  "accountId": "81de44cb-4b70-4207-9f5e-54355491a5cc",
  "currency": "EUR",
  "authAmount": 10,
  "captureAmount": 10,
  "refundAmount": 0,
  "state": "Capture",
  "paymentId": "597ed30f-9510-4990-ad07-73135ba8fcb3",
  "finalCapture": true,
  "created": "2026-10-16T09:12:44.381Z"
}

Checklist for your endpoint

  • Answer 200 quickly, then do the work
  • Verify the sha512 header (or restrict the source address)
  • Be safe against the same webhook arriving more than once and out of order
  • Read agreement.status and agreement.reason if you save cards
  • Reconcile with Payment/GetPaymentStatus for anything you did not hear about
In this article
Back to top Generated by DocFX