Receiving data from external services through callback URLs and webhooks is a fundamental requirement for payment gateway integrations, third party notification systems, and any WordPress project that needs to process asynchronous data from external APIs. This guide covers how to build a reliable callback endpoint in WordPress using the REST API, what authentication and validation checks to implement, how to handle idempotency and replay safety, what response codes to return and why they matter, and where WordPress developers most commonly trip up when building these endpoints. The patterns here apply to payment confirmations, shipping status updates, CRM synchronization, and any scenario where an external service sends data to your WordPress site. For background on how HTTP requests and web API fundamentals work, the MDN HTTP documentation provides a solid foundation. The WooCommerce hub links to the rest of the payment integration series.
What a Callback Endpoint Is
A callback endpoint is a URL on your server that an external service sends data to after processing something on their side. The most common example in WooCommerce is the payment callback: your site sends a payment request to a provider, the provider processes the payment, and then the provider sends the result back to your callback URL. Your endpoint receives that result, validates it, matches it to the correct order, and updates the order status accordingly.
The key difference between a callback and a regular API request is the direction of initiation. In a regular API call, your server initiates the request and receives a synchronous response. In a callback flow, your server registers the URL, and the external service initiates the request when it is ready. This means your endpoint must be publicly accessible, must be able to handle requests at any time, and must be resilient to retries, delays, and out of order delivery.
Webhook Basics in Plain Language
A webhook is a callback pattern where the external service sends a notification to your URL whenever a specific event occurs on their platform. Payment completed, subscription cancelled, inventory updated, user action triggered. Instead of your server polling the external service to check for updates, the external service pushes the update to you.
Webhooks are preferred over polling because they are more efficient, more timely, and generate less unnecessary API traffic. But they introduce specific engineering requirements: your endpoint must be reliable because missed webhooks mean missed data, you need to handle the case where the external service retries a webhook you already processed, and you need to verify that the webhook actually came from the service it claims to come from and was not forged by a malicious actor.
Building the Endpoint in WordPress
The WordPress REST API provides a clean way to register custom endpoints. For callback and webhook handling, register an endpoint under a custom namespace that accepts POST requests and implements your validation logic in the permission callback and your processing logic in the main callback.
add_action( 'rest_api_init', function() {
register_rest_route( 'custom-gateway/v1', '/callback', array(
'methods' => 'POST',
'callback' => 'process_gateway_callback',
'permission_callback' => 'verify_gateway_callback',
));
});
function verify_gateway_callback( WP_REST_Request $request ) {
$signature = $request->get_header( 'X-Provider-Signature' );
$raw_body = $request->get_body();
if ( empty( $signature ) ) {
return new WP_Error(
'missing_signature',
'Request signature is missing.',
array( 'status' => 401 )
);
}
$secret = get_option( 'custom_gateway_webhook_secret' );
$expected = hash_hmac( 'sha256', $raw_body, $secret );
if ( ! hash_equals( $expected, $signature ) ) {
return new WP_Error(
'invalid_signature',
'Request signature verification failed.',
array( 'status' => 403 )
);
}
return true;
}
function process_gateway_callback( WP_REST_Request $request ) {
$data = $request->get_json_params();
if ( empty( $data['transaction_id'] ) || empty( $data['status'] ) ) {
return new WP_REST_Response(
array( 'error' => 'Invalid payload structure.' ),
400
);
}
$txn_id = sanitize_text_field( $data['transaction_id'] );
$status = sanitize_text_field( $data['status'] );
// Find the matching order
$orders = wc_get_orders( array(
'meta_key' => '_gateway_txn_id',
'meta_value' => $txn_id,
'limit' => 1,
));
if ( empty( $orders ) ) {
return new WP_REST_Response(
array( 'error' => 'Order not found for transaction.' ),
404
);
}
$order = $orders[0];
// Idempotency check
if ( $order->get_meta( '_callback_processed' ) === 'yes' ) {
return new WP_REST_Response(
array( 'message' => 'Already processed.' ),
200
);
}
if ( $status === 'completed' ) {
$order->payment_complete( $txn_id );
$order->add_order_note( 'Payment confirmed via webhook callback.' );
} elseif ( $status === 'failed' ) {
$order->update_status( 'failed', 'Payment failed per webhook notification.' );
}
$order->update_meta_data( '_callback_processed', 'yes' );
$order->save();
return new WP_REST_Response( array( 'message' => 'Processed.' ), 200 );
} Authentication and Validation Checks
The permission callback is your first line of defense. At minimum, verify that the request contains the expected authentication header. Most webhook providers sign their requests with an HMAC using a shared secret. Some providers use API keys in headers. Others use IP allowlisting. Whatever mechanism the provider uses, implement it in the permission callback so that invalid requests are rejected before your processing logic runs.
Beyond authentication, validate the payload structure before processing it. Check that required fields exist, that field values are in expected formats, and that identifiers match records in your database. Do not assume that just because the request passed authentication it contains valid data. Authenticated requests with malformed payloads can still cause problems if your processing code does not handle them gracefully.
Use hash_equals for signature comparison rather than direct string comparison. Direct comparison is vulnerable to timing attacks where an attacker can gradually determine the correct signature by measuring response time differences. hash_equals performs constant time comparison that does not leak information through timing.
Logging and Observability
Log every callback your endpoint receives. Log the timestamp, the source IP, the HTTP method, the presence or absence of authentication headers, the request body structure (not sensitive field values), and the processing outcome. This log is your diagnostic tool when callbacks are not being processed correctly.
What to log: transaction IDs, order numbers, processing status, validation failures, and error conditions. What not to log: API secrets, full credit card numbers, authentication tokens, or personally identifiable customer data. The log should give you enough information to trace what happened without creating a security liability.
In WordPress, you can use the built in error_log function for basic logging, or write to a custom log file in the uploads directory. For production systems handling significant transaction volume, consider structured logging that can be queried and analyzed more easily than plain text files.
Response Codes
The HTTP response code your endpoint returns matters more than most developers realize. Webhook providers use the response code to determine whether the delivery was successful. A 200 response tells the provider the callback was received and processed. A 4xx response tells the provider the request was invalid and should not be retried. A 5xx response or a timeout tells the provider something went wrong on your side and the callback should be retried.
If your endpoint returns a 500 when it encounters a processing error, the provider will retry. If the processing error is permanent, like an order not found or an invalid payload, those retries will keep failing and generating noise. Use 400 for invalid payloads and 404 for missing orders so the provider knows not to retry. Use 200 for successfully processed callbacks and for idempotent repeats of already processed callbacks.
Some providers have specific requirements about what the response body should contain. Check the provider's webhook documentation for response format expectations. A generic JSON response with a message field is usually sufficient, but some providers expect specific field names in the response.
Idempotency and Replay Safety
Idempotency means that processing the same callback multiple times produces the same result as processing it once. This is critical for payment callbacks because providers can and do send the same callback more than once. Network timeouts, provider retry logic, and infrastructure issues all cause duplicate delivery.
The simplest idempotency pattern is to mark the record as processed after the first successful callback and check for that mark before processing subsequent callbacks. In the code example above, the _callback_processed meta field serves this purpose. When a duplicate callback arrives, the endpoint checks the meta field, sees that processing already completed, and returns a 200 response without doing anything.
A more robust approach uses the transaction ID itself as the idempotency key and stores the processing result along with it. This lets you return the same response for duplicate callbacks and also provides an audit trail of callback delivery attempts.
Where Developers Trip Up in WordPress
The most common WordPress specific mistakes in callback endpoint implementation are: registering endpoints with overly permissive permission callbacks that accept any request, using admin_ajax.php instead of the REST API which causes unnecessary WordPress admin bootstrapping overhead, not sanitizing input data before using it in database queries, not returning proper HTTP status codes which causes the provider to retry permanently, storing the webhook secret in code rather than in the database or environment variable, and not accounting for WordPress cron or object cache behavior that can affect order status reads.
Another subtle issue is WordPress's REST API authentication model. By default, REST API requests from external services are not authenticated as any WordPress user. Your endpoint runs with no current user context. If your processing logic depends on current_user_can or other capability checks, those will fail. Design your callback processing to work without user context, relying on the webhook signature for authorization instead.
Finally, be aware of hosting environments that aggressively cache or proxy API endpoints. Some managed WordPress hosts cache GET requests to REST API endpoints by default. While your POST callback endpoint should not be affected, it is worth verifying that your hosting environment does not interfere with incoming webhook requests.
For the complete payment gateway tutorial that covers how callback endpoints integrate with the broader checkout and payment flow, see building a custom WooCommerce payment gateway plugin. For testing patterns specific to sandbox environments, see the sandbox testing guide.