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.
- Activepieces wait for webhook: resume URL as Sume webhook_url
Pass an Activepieces waitpoint resume URL as Sume's webhook_url and the paused run resumes on the terminal job event. Know the retries and signature limits.
- Activepieces webhook authentication failed run: Sume signature
Activepieces 0.92 shows a failed run when webhook authentication fails. Sume sends an HMAC signature header you verify yourself in a code step.
Written by Sume