processor = $processor; } /** * Register the webhook receiver route. * * @since 1.0.0 * * @return void */ public function registerRoutes(): void { register_rest_route( self::API_NAMESPACE, '/webhooks/(?P[a-z0-9_\-]+)', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'receive' ), // Public because payment gateways call it; the gateway verifies the delivery signature first. 'permission_callback' => '__return_true', 'args' => array( 'gateway' => array( 'description' => __( 'The gateway id this delivery comes from.', 'releasecart' ), 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ), ), ), ) ); } /** * Verify, deduplicate and dispatch one gateway event. * * Keep raw body bytes for signature verification. * * @since 1.0.0 * * @param WP_REST_Request $request The incoming request. * @return WP_REST_Response|WP_Error The acknowledgment, or an error envelope. */ public function receive( WP_REST_Request $request ) { $status = $this->processor->process( (string) $request->get_param( 'gateway' ), new RequestData( (string) $request->get_body(), $request->get_headers() ) ); if ( 404 === $status ) { return $this->notFound( __( 'Unknown payment gateway.', 'releasecart' ) ); } if ( 400 === $status ) { return $this->error( 'releasecart_invalid_signature', __( 'Webhook signature verification failed.', 'releasecart' ), 400 ); } if ( $status >= 500 ) { // Answering 5xx makes the gateway retry the delivery; the claim protocol // guarantees the retry is reprocessed. return $this->serverError( __( 'Webhook processing failed. The gateway will retry.', 'releasecart' ) ); } return new WP_REST_Response( array( 'received' => true ), 200 ); } }