WordPress webhook endpoint: receive Sume video callbacks
Register a REST route with register_rest_route, verify the signature on the raw body in PHP, and answer 2xx so Sume stops retrying.

A WordPress webhook endpoint is a REST route: call register_rest_route() inside an rest_api_init callback, set 'methods' => 'POST', and give it a permission_callback. A webhook sender isn't a logged-in user, so the route is public and __return_true is the documented permission callback for that; the security comes from checking the signature yourself. For Sume, that means an HMAC-SHA256 check on the raw body.
WordPress facts come from the REST API handbook's Adding Custom Endpoints and the reference pages for get_body(), get_header() and WP_REST_Response, plus the PHP manual for hash_hmac and hash_equals, all read 2026-09-29. Sume facts come from Webhooks. Sume has no WordPress plugin, so this is plain PHP.
How do I register a public webhook route in WordPress?
Register the route on rest_api_init, with a namespace, a route and an options array. The handbook says that as of WordPress 5.5 a route without a permission_callback triggers a _doing_it_wrong notice, and that intended-public routes should use __return_true. The route below answers at /wp-json/acme/v1/sume.
add_action( 'rest_api_init', function () {
register_rest_route( 'acme/v1', '/sume', array(
'methods' => 'POST',
'callback' => 'acme_sume_webhook',
'permission_callback' => '__return_true',
) );
} );
function acme_sume_webhook( WP_REST_Request $request ) {
$raw = $request->get_body(); // the body exactly as it arrived
$ok = acme_verify_sume(
$raw,
(string) $request->get_header( 'x-sume-webhook-timestamp' ),
(string) $request->get_header( 'x-sume-webhook-signature' ),
(string) getenv( 'SUME_COM_WEBHOOK_SIGNING_SECRET' )
);
if ( ! $ok ) {
return new WP_REST_Response( array( 'error' => 'bad signature' ), 401 );
}
$event = json_decode( $raw, true );
// Store $event durably here, keyed by $event['job_id'], then answer.
return new WP_REST_Response( null, 204 );
}How do I read the raw body and the signature header?
WP_REST_Request::get_body() returns the request body content as a string of binary data, and get_header( $key ) returns the header value or null, with the name canonicalized to lowercase. Verify the string from get_body(). Sume signs the raw JSON body, so a value you decoded and re-encoded can differ in key order or spacing and won't verify.
| Piece | Where it comes from | Used for |
|---|---|---|
| Raw body | $request->get_body() | The bytes Sume signed |
x-sume-webhook-timestamp | $request->get_header() | Replay window; part of the signed string |
x-sume-webhook-signature | $request->get_header() | sume-v1=<hex>, possibly several comma-separated entries |
| Signing secret | Dashboard Webhooks tab, kept outside the theme | The HMAC key |
How do I verify the Sume signature in PHP?
Sume signs HMAC-SHA256 over <timestamp>.<raw_body> and sends sume-v1=<hex_signature>. During a secret rotation the header carries one entry per live secret, comma-separated, so accept the delivery when any entry matches. hash_hmac() returns lowercase hex by default. Compare with hash_equals(), known string first and the user-supplied string second, because the manual says a plain === leaks timing. Refuse an empty secret: an empty HMAC key would let a forged signature verify.
function acme_verify_sume( $raw, $ts, $header, $secret ) {
if ( '' === $secret || ! is_numeric( $ts ) ) {
return false;
}
if ( abs( time() - (int) $ts ) > 300 ) { // five-minute replay window
return false;
}
$expected = 'sume-v1=' . hash_hmac( 'sha256', $ts . '.' . $raw, $secret );
$matched = false;
foreach ( explode( ',', $header ) as $entry ) {
if ( hash_equals( $expected, trim( $entry ) ) ) {
$matched = true; // keep looping: compare every entry
}
}
return $matched;
}What should the route answer, and what does Sume do next?
- Answer any
2xxafter you've stored the event. Sume retries network errors and non-2xx responses, up to 10 attempts in total, about 30 seconds apart by default, with 10 seconds allowed per attempt. - Do slow work after the response, or in a scheduled task. A slow endpoint burns the 10-second budget and gets retried.
- Dedupe on
job_id: retries repeat it. Use it as the idempotency key on your side. - The webhook URL must be public HTTPS. Localhost and private-network URLs are rejected, so a local WordPress needs a public HTTPS tunnel while you test.
- Sume sends terminal events only:
job.completed,job.failedandjob.canceled. Keep polling the job's status URL for deliveries that never arrive.
Sources
- Webhooks
- Jobs and results
- Verifying webhooks
- WordPress: Adding Custom Endpoints (read 2026-09-29)
- WordPress: WP_REST_Request::get_body() (read 2026-09-29)
- WordPress: WP_REST_Request::get_header() (read 2026-09-29)
- WordPress: WP_REST_Response (read 2026-09-29)
- PHP manual: hash_equals (read 2026-09-29)
- PHP manual: hash_hmac (read 2026-09-29)
Related posts
More in Integrations
- Workato HTTP connector: call an API with a key and JSON
Workato's HTTP connector calls any HTTP API: a Header auth connection holds the key, and Send request via HTTP POSTs a JSON body and maps the reply.
- wp_remote_post timeout: calling a video API from WordPress
wp_remote_post waits 5 seconds by default. Set timeout yourself, submit the video job, and let a callback deliver the result instead of waiting.
- How to add an MCP server to ChatGPT with developer mode
Turn on ChatGPT developer mode, create an app for the server's URL, and sign in with OAuth. The steps, with Sume's hosted MCP server as the example.
- How to add subtitles to a video in Python
Add subtitles to a video in Python with Requests: POST the video URL to Sume's /v1/video-captions, poll the job, then read the captioned video_url.
Written by Sume