Testing a WooCommerce payment gateway integration in a sandbox environment is the only safe way to validate the implementation before it handles real transactions. But sandbox testing has its own patterns, failure modes, and pitfalls that are different from production debugging. This guide covers the practical testing patterns that make sandbox testing reliable: payload validation before each request, independent request signing verification, PHP cURL request handling basics, and using cURL to simulate callback delivery without waiting for the provider. It also covers the common sandbox false positives that give misleading confidence in integrations that will fail in production. For the PHP cURL documentation covering the functions used in these test patterns, see PHP.net cURL. The WooCommerce hub links to the full payment integration series.
What to Test and at What Layer
Payment gateway testing happens at multiple levels and each level tests something different. Unit tests verify individual functions in isolation: does the payload builder format the amount correctly, does the signature function produce the expected hash, does the callback parser extract the transaction ID. Integration tests verify that your gateway class communicates with the provider's sandbox API correctly. End to end tests verify the complete checkout flow from customer payment selection through order confirmation.
Most developers focus on end to end testing and skip the unit and integration layers. The problem with this approach is that when the end to end test fails, you have no visibility into which layer contains the bug. A failed checkout could be a payload formatting error, a signing error, a network error, or a callback handling error. Each of those has a different fix and a different risk profile.
Build testing in layers. Start with unit tests on your payload builder and signing functions. Then verify the integration against the sandbox API at the HTTP request level using direct cURL commands. Then run the full checkout flow. This sequence isolates problems to specific layers and makes debugging faster.
Payload Validation Before Every Request
Before sending a request to the provider, validate the payload against the provider's documented requirements. This seems obvious, but it saves significant debugging time compared to sending the request and trying to interpret the error response.
function validate_payment_payload( $payload ) {
$errors = array();
if ( empty( $payload['amount'] ) || ! is_numeric( $payload['amount'] ) ) {
$errors[] = 'Amount must be a numeric value.';
}
if ( $payload['amount'] <= 0 ) {
$errors[] = 'Amount must be positive.';
}
if ( empty( $payload['currency'] ) || strlen( $payload['currency'] ) !== 3 ) {
$errors[] = 'Currency must be a 3-letter ISO code.';
}
if ( empty( $payload['phone'] ) ) {
$errors[] = 'Phone number is required.';
} elseif ( ! preg_match( '/^[0-9]{10,15}$/', $payload['phone'] ) ) {
$errors[] = 'Phone number must be 10-15 digits.';
}
if ( empty( $payload['reference'] ) ) {
$errors[] = 'Merchant reference is required.';
} elseif ( strlen( $payload['reference'] ) > 20 ) {
$errors[] = 'Merchant reference exceeds max length.';
}
return $errors;
} Run this validation before every request during development and sandbox testing. In production, you can reduce it to critical field validation for performance, but during development, validate everything. Catching a formatting error before it reaches the provider is faster than parsing the provider's error response to figure out which field was wrong.
Request Signing Verification
If the provider requires request signing, test the signing logic independently. Generate a test payload with known values, compute the signature, and compare it against a known correct signature. If the provider publishes test vectors or example signed requests in their documentation, use those as your validation baseline.
// Test signing with known values
$test_payload = '{"amount":5000,"currency":"UGX","reference":"TEST001"}';
$test_secret = 'test_secret_key_123';
$expected_sig = hash_hmac( 'sha256', $test_payload, $test_secret );
echo "Expected: " . $expected_sig . "\n";
// Verify your signing function produces the same result
$computed_sig = compute_request_signature( $test_payload, $test_secret );
echo "Computed: " . $computed_sig . "\n";
echo "Match: " . ( $expected_sig === $computed_sig ? 'YES' : 'NO' ) . "\n"; Common signing mistakes include: sorting JSON keys before signing when the provider expects the keys in the order they appear, including or excluding whitespace in the signed string, encoding the signature as hex versus base64, and using the wrong HMAC algorithm. If the provider's error message says "invalid signature" but your computed signature looks correct, check each of these variables.
PHP Request Handling Basics
Payment gateway requests in WordPress typically use the wp_remote_post and wp_remote_get functions from the HTTP API. For testing purposes, it helps to understand what these functions do under the hood and how to replicate them with cURL for direct sandbox testing.
// WordPress HTTP API request
$response = wp_remote_post( $api_url, array(
'timeout' => 30,
'headers' => array(
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $api_key,
),
'body' => wp_json_encode( $payload ),
));
// Equivalent cURL for testing outside WordPress
// curl -X POST https://sandbox.provider.com/api/pay \
// -H "Content-Type: application/json" \
// -H "Authorization: Bearer YOUR_KEY" \
// -d '{"amount":5000,"currency":"UGX","reference":"TEST001"}' \
// -v
The -v flag on cURL shows the full request and response headers, which is invaluable for debugging. You can see exactly what went over the wire, including the SSL handshake, the request headers your code sent, and the complete response including status code and headers. Compare this against what wp_remote_post reports to identify any differences in how WordPress constructs the request.
Using cURL to Simulate Callback Delivery
To test your callback endpoint without waiting for the provider to send a callback, simulate one with cURL. Build a callback payload that matches the format the provider sends and POST it to your callback endpoint.
# Simulate a successful payment callback
curl -X POST https://your-dev-site.local/wp-json/custom-gateway/v1/callback \
-H "Content-Type: application/json" \
-H "X-Provider-Signature: YOUR_COMPUTED_SIGNATURE" \
-d '{
"transaction_id": "TXN_TEST_001",
"status": "completed",
"amount": 5000,
"currency": "UGX"
}' \
-v
# Simulate a failed payment callback
curl -X POST https://your-dev-site.local/wp-json/custom-gateway/v1/callback \
-H "Content-Type: application/json" \
-H "X-Provider-Signature: YOUR_COMPUTED_SIGNATURE" \
-d '{
"transaction_id": "TXN_TEST_002",
"status": "failed",
"amount": 5000,
"error_code": "INSUFFICIENT_FUNDS"
}' \
-v
# Test with invalid signature to verify rejection
curl -X POST https://your-dev-site.local/wp-json/custom-gateway/v1/callback \
-H "Content-Type: application/json" \
-H "X-Provider-Signature: invalid_signature_value" \
-d '{"transaction_id":"TXN_TEST_003","status":"completed"}' \
-v Run each of these scenarios and verify: the response code is correct, the order status was updated correctly, the order note was added, and the log entry was created. Also test with a duplicate callback for the same transaction to verify idempotency.
Common Sandbox False Positives
Sandbox environments are helpful but they are not production mirrors. Here are the specific sandbox behaviors that create false confidence during testing.
Sandbox accepts any amount. Production may have minimum and maximum transaction limits that sandbox does not enforce. Test with amounts near the limits if you know what they are.
Sandbox returns faster. If your timeout handling is tuned for sandbox speeds, production timeouts will trigger prematurely. Always set timeouts based on what you expect in production, not what you observe in sandbox.
Sandbox does not validate all fields. Some sandbox environments skip validation on optional fields that production requires. Send complete payloads during sandbox testing even if sandbox does not seem to need all the fields.
Sandbox callback timing is different. Sandbox may send callbacks immediately while production sends them after actual payment processing, which can take seconds to minutes. Your callback handler needs to work regardless of when the callback arrives relative to the initial request.
Sandbox error codes are generic. When something fails in sandbox, the error code or message may not match what production sends for the same failure. Do not hardcode error handling logic based on sandbox error codes alone.
Sandbox does not retry callbacks. Production providers typically retry failed callback deliveries several times with exponential backoff. Your callback endpoint needs to handle retries correctly, including idempotency for already processed transactions.
The lesson is not that sandbox testing is useless. It is that sandbox testing tells you your integration probably works. Production testing tells you it actually works. Treat sandbox success as a prerequisite for production rollout, not as proof of production readiness.
For the complete gateway implementation guide, see building a custom WooCommerce payment gateway plugin. For the production transition checklist, see integrating from sandbox to production. For callback endpoint architecture, see the webhook and callback endpoint guide.