Webhooks
The PaySuite Webhook service triggers events that SoftCo products can subscribe to.
To subscribe to webhook events, a publicly accessible HTTPS endpoint and signing key must be provided to PaySuite.
Events
Webhook events are produced when the status of the customer onboarding changes. The following status changes will trigger a webhook event.
- Credit Risk status approved or rejected
- Stripe data capture completed
- Stripe KYC status active
Security
All webhook events are signed. The event is sent with a HTTP header containing a hash of the payload. Upon receiving the event, SoftCo products must validate the signature.
The signing key is generated by the subscribing product and provided to PaySuite together with the endpoint URL. It is a shared secret: it is never included in the request, and the same key is used by PaySuite to sign and by the subscriber to verify.
Signature validation
- Read the raw request body before deserialising it. The signature is computed over the exact bytes that were sent, so parsing and re-serialising the JSON will invalidate it.
- Compute
HMAC-SHA256of the raw body using the shared signing key. - Encode the result as Base64.
- Compare with the value of the
X-APS-Signatureheader using a constant-time comparison. The header contains the Base64 digest only - there is nosha256=prefix and the digest is not hex encoded. - Only process the webhook if the signatures match; otherwise reject the request and do not process the payload.
Any proxy or middleware that rewrites, re-formats or pretty-prints the request body will change the bytes and cause verification to fail.
Example verification (Node.js)
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('base64');
const received = Buffer.from(signature ?? '', 'utf8');
const expected = Buffer.from(expectedSignature, 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
Example verification (C#)
public static string ComputeHmacSha256Signature(string payload, string secretKey)
{
using var hmacSha256 = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
var hashBytes = hmacSha256.ComputeHash(Encoding.UTF8.GetBytes(payload));
var signature = Convert.ToBase64String(hashBytes);
return signature;
}
public static bool VerifyWebhookSignature(string rawBody, string receivedSignature, string secretKey)
{
var expectedSignature = ComputeHmacSha256Signature(rawBody, secretKey);
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expectedSignature),
Encoding.UTF8.GetBytes(receivedSignature ?? string.Empty));
}
Data structure
All webhook payloads follow a consistent JSON structure:
| Field | Type | Description |
|---|---|---|
MerchantId | String | ID used for future queries to obtain the onboarding status. |
CustomerId | String | As provided by SoftCo |
SessionId | String | ID created by APS to track the session |
Type | String | "merchant.updated" |
Data | Object | Returns the CreditRisk object and the Stripe object, plus Evo data if the merchant is linked to an EVO organisation. |
Example JSON
{
"MerchantId": "<string>",
"CustomerId": "<string>",
"SessionId": "<uuid>",
"Type": "merchant.updated",
"Data": {
"CreditRisk": {
"MerchantId": "<string>",
"DataCapture": {
"Status": "<Pending|Complete>",
"UpdatedDate": "<dateTimeUTC>"
},
"Cra": {
"Status": "<Approved|Rejected|Pending>",
"UpdatedDate": "<dateTimeUTC>"
}
},
"Stripe": {
"ConnectAccountId": "<string>",
"DataCapture": {
"Status": "<Pending|Complete>",
"UpdatedDate": "<dateTimeUTC>"
},
"Kyc": {
"Status": "<Active|Inactive|Pending|Restricted|null>",
"UpdatedDate": "<dateTimeUTC>",
"PaymentsEnabled": "<true|false>",
"PayoutsEnabled": "<true|false>"
}
},
"Evo": {
"OrganisationId": "<string|null>",
"CustomerReference": "<string|null>"
}
}
}
Payload handling
The SoftCo product should handle the webhook payload as follows. The data returned will help to answer the following questions.
Is data capture pending?
When a customer begins onboarding, the status of data capture will be Pending, indicating that the customer must provide information before payment processing can be enabled.
There are two data capture status fields that indicate the stage of onboarding
CreditRisk.DataCapture.StatusStripe.DataCapture.Status
If either field is Pending, then the SoftCo product must perform an API request to the PUT /merchants/{id} endpoint. The response will return the next action to be performed in order to complete data capture.
If both fields are Complete, then no further information is required from the customer at this stage. Note: the status may change later.
Are payments enabled?
Before enabling payment processing in the SoftCo product for the customer, the status of credit risk assessment and know-your-customer must be verified.
The following fields will indicate the statuses
CreditRisk.Cra.Status- must beApprovedStripe.Kyc.Status- must beActive
If both fields are not as set above, then payments must be prevented.
An example response would look like this.
{
"MerchantId": "82385787-d11f-4c82-835f-ae7cb5ead49a",
"CustomerId": "testDemo1455",
"SessionId": "428302d529a0783bd4c36014bdf1ff97",
"Type": "merchant.updated",
"Data": {
"CreditRisk": {
"MerchantId": "82385787-d11f-4c82-835f-ae7cb5ead49a",
"DataCapture": {
"Status": "Complete",
"UpdatedDate": "2025-03-18T16:06:27.0888238"
},
"Cra": {
"Status": "Approved",
"UpdatedDate": "2025-03-18T16:06:27.089065"
}
},
"Stripe": {
"ConnectAccountId": "acct_stripe123",
"DataCapture": {
"Status": "Complete",
"UpdatedDate": "2025-03-18T16:06:31.1352286"
},
"Kyc": {
"Status": "Active",
"UpdatedDate": "2025-03-18T16:06:31.1352287",
"PaymentsEnabled": true,
"PayoutsEnabled": true
}
},
"Evo": {
"OrganisationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"CustomerReference": "CU12345"
}
}
}
Is the customer rejected?
On some occasions, the risk of onboarding a customer may be too high and therefore rejected from onboarding onto the platform.
The following fields will indicate the status
CreditRisk.Cra.Status- must beRejected
There will be no further indication identifying the reason for rejection. This will be communicated to the customer via Risk & Compliance.
An example response would look like this.
{
"MerchantId": "a9818e68-7494-4567-915f-01cff025f5d4",
"CustomerId": "testDemo1335",
"SessionId": "1e02a869b19fcc164ac94ffd9423a718",
"Type": "merchant.updated",
"Data": {
"CreditRisk": {
"MerchantId": "a9818e68-7494-4567-915f-01cff025f5d4",
"DataCapture": {
"Status": "Complete",
"UpdatedDate": "2025-03-18T15:49:49.9136124"
},
"Cra": {
"Status": "Rejected",
"UpdatedDate": "2025-03-18T15:49:49.9136172"
}
}
}
}
Retry strategy
A retry strategy will be implemented to make a reasonable best effort delivery guarantee. To support retries, a unique identifier will be generated to allow duplicate detection at the Division.
Timestamp Format
All timestamps use ISO 8601 format with UTC timezone:
- Format:
YYYY-MM-DDTHH:mm:ssZ - Example:
2024-01-16T14:30:00Z