Skip to main content

Webhooks

The PaySuite Webhook service triggers events that SoftCo products can subscribe to.

info

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​

  1. 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.
  2. Compute HMAC-SHA256 of the raw body using the shared signing key.
  3. Encode the result as Base64.
  4. Compare with the value of the X-APS-Signature header using a constant-time comparison. The header contains the Base64 digest only - there is no sha256= prefix and the digest is not hex encoded.
  5. Only process the webhook if the signatures match; otherwise reject the request and do not process the payload.
caution

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:

FieldTypeDescription
MerchantIdStringID used for future queries to obtain the onboarding status.
CustomerIdStringAs provided by SoftCo
SessionIdStringID created by APS to track the session
TypeString"merchant.updated"
DataObjectReturns 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.Status
  • Stripe.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 be Approved
  • Stripe.Kyc.Status - must be Active

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 be Rejected

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