Validate Requests
This document explains how to verify webhook requests from QuickPay using public key cryptography to ensure the authenticity and integrity of webhook data.
Overview
QuickPay uses private key cryptography to sign webhook requests, ensuring that:
- Authenticity: The webhook is genuinely from QuickPay
- Integrity: The data hasn't been tampered with during transmission
- Non-repudiation: QuickPay cannot deny sending the webhook
To validate the signature in Production workspaces, please use the public key below:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
Verification Process
1. Extract Headers
Extract the following headers from the webhook request:
Header | Description |
---|---|
X-Webhook-Signature | Base64-encoded signature (signed with QuickPay's private key) |
X-Webhook-Timestamp | Unix timestamp of the webhook |
X-Webhook-Trace-ID | Unique trace ID for idempotency |
2. Create Payload String
Use the raw JSON body of the webhook request directly:
{request_body}
Where:
request_body
is the raw JSON body of the webhook request
3. Verify Signature
Use QuickPay's public key to verify the signature (which was created using QuickPay's private key):
Code Example
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, publicKey) {
try {
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(payload);
return verifier.verify(publicKey, signature, 'base64');
} catch (error) {
console.error('Signature verification failed:', error);
return false;
}
}
function verifyWebhookSignature($payload, $signature, $publicKey) {
try {
$publicKeyResource = openssl_pkey_get_public($publicKey);
if (!$publicKeyResource) {
throw new Exception('Invalid public key');
}
$signatureBytes = base64_decode($signature);
$result = openssl_verify($payload, $signatureBytes, $publicKeyResource, OPENSSL_ALGO_SHA256);
openssl_free_key($publicKeyResource);
return $result === 1;
} catch (Exception $e) {
error_log("Signature verification failed: " . $e->getMessage());
return false;
}
}
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.nio.charset.StandardCharsets;
public class WebhookVerifier {
private final PublicKey publicKey;
public WebhookVerifier(String publicKeyPem) throws Exception {
// Remove header and footer, and decode
String publicKeyPEM = publicKeyPem
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
this.publicKey = keyFactory.generatePublic(keySpec);
}
public boolean verifySignature(String payload, String signature) {
try {
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(payload.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = Base64.getDecoder().decode(signature);
return sig.verify(signatureBytes);
} catch (Exception e) {
System.err.println("Signature verification failed: " + e.getMessage());
return false;
}
}
}
Webhook Response Requirements
HTTP Status Codes
Status Code | Description | QuickPay Behavior |
---|---|---|
200 | Success | Stop retrying, webhook processed successfully |
4xx | Client Error | Stop retrying, webhook will not be retried |
5xx | Server Error | Retry webhook with exponential backoff |
Response Format
Always return a JSON response with a success
field:
{
"success": true
}
QuickPay Retry Mechanism
Retry Schedule
If your webhook endpoint returns a 5xx status code or fails to respond, QuickPay will retry with the following schedule:
Attempt | Delay | Total Time |
---|---|---|
1st | Immediate | 0s |
2nd | 30 seconds | 30s |
3rd | 1 minute | 1m 30s |
4th | 2 minutes | 3m 30s |
5th | 4 minutes | 7m 30s |
6th | 8 minutes | 15m 30s |
7th | 16 minutes | 31m 30s |
8th | 32 minutes | 1h 3m 30s |
9th | 64 minutes | 2h 7m 30s |
10th | 128 minutes | 4h 15m 30s |
Maximum Retries: 10 attempts over approximately 4 hours
Retry Headers
Each retry will include the same headers as the original request, plus:
X-Webhook-Retry-Count
: Current retry attempt numberX-Webhook-Retry-Attempt
: Timestamp of this retry attempt
Security Best Practices
1. Always Verify Signatures
- Never process webhooks without signature verification
- Use the official QuickPay public key (to verify signatures created with QuickPay's private key)
- Implement proper error handling for verification failures
2. Check Timestamp
- Verify the timestamp is recent (within 5 minutes)
- Prevent replay attacks
3. Implement Idempotency
- Use the
X-Webhook-Trace-ID
for idempotency - Store processed trace IDs to prevent duplicates
4. Secure Storage
- Store QuickPay's public key securely
- Rotate keys when necessary
- Use environment variables for sensitive data
5. Error Handling
- Log verification failures
- Return appropriate HTTP status codes
- Implement monitoring and alerting
Testing
Test with cURL
# Test webhook endpoint
curl -X POST "https://your-domain.com/webhook" \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: test_signature" \
-H "X-Webhook-Timestamp: 1642239000" \
-H "X-Webhook-Trace-ID: test_trace_id" \
-d '{
"event_type": "payment.created",
"event_id": "evt_test_1234567890",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"payment_id": "pay_test_1234567890",
"amount": "100.00",
"currency": "USD",
"status": "pending"
}
}'
Updated 24 days ago