curl --request POST \
--url https://{domain}/wp-json/sagescreen/v1/stripe/webhook \
--header 'Content-Type: application/json' \
--header 'Stripe-Signature: <api-key>' \
--data '
{
"id": "evt_1NqIAB2eZvKYlo2C5XFo8Dg6",
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_test_a1b2c3d4",
"customer": "cus_Abc123",
"payment_status": "paid",
"metadata": {
"council_id": "c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6",
"sku": "P-25-2"
}
}
}
}
'import requests
url = "https://{domain}/wp-json/sagescreen/v1/stripe/webhook"
payload = {
"id": "evt_1NqIAB2eZvKYlo2C5XFo8Dg6",
"type": "checkout.session.completed",
"data": { "object": {
"id": "cs_test_a1b2c3d4",
"customer": "cus_Abc123",
"payment_status": "paid",
"metadata": {
"council_id": "c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6",
"sku": "P-25-2"
}
} }
}
headers = {
"Stripe-Signature": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Stripe-Signature': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
id: 'evt_1NqIAB2eZvKYlo2C5XFo8Dg6',
type: 'checkout.session.completed',
data: {
object: {
id: 'cs_test_a1b2c3d4',
customer: 'cus_Abc123',
payment_status: 'paid',
metadata: {council_id: 'c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6', sku: 'P-25-2'}
}
}
})
};
fetch('https://{domain}/wp-json/sagescreen/v1/stripe/webhook', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{domain}/wp-json/sagescreen/v1/stripe/webhook",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'id' => 'evt_1NqIAB2eZvKYlo2C5XFo8Dg6',
'type' => 'checkout.session.completed',
'data' => [
'object' => [
'id' => 'cs_test_a1b2c3d4',
'customer' => 'cus_Abc123',
'payment_status' => 'paid',
'metadata' => [
'council_id' => 'c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6',
'sku' => 'P-25-2'
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Stripe-Signature: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://{domain}/wp-json/sagescreen/v1/stripe/webhook"
payload := strings.NewReader("{\n \"id\": \"evt_1NqIAB2eZvKYlo2C5XFo8Dg6\",\n \"type\": \"checkout.session.completed\",\n \"data\": {\n \"object\": {\n \"id\": \"cs_test_a1b2c3d4\",\n \"customer\": \"cus_Abc123\",\n \"payment_status\": \"paid\",\n \"metadata\": {\n \"council_id\": \"c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6\",\n \"sku\": \"P-25-2\"\n }\n }\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Stripe-Signature", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://{domain}/wp-json/sagescreen/v1/stripe/webhook")
.header("Stripe-Signature", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"id\": \"evt_1NqIAB2eZvKYlo2C5XFo8Dg6\",\n \"type\": \"checkout.session.completed\",\n \"data\": {\n \"object\": {\n \"id\": \"cs_test_a1b2c3d4\",\n \"customer\": \"cus_Abc123\",\n \"payment_status\": \"paid\",\n \"metadata\": {\n \"council_id\": \"c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6\",\n \"sku\": \"P-25-2\"\n }\n }\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://{domain}/wp-json/sagescreen/v1/stripe/webhook")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Stripe-Signature"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"id\": \"evt_1NqIAB2eZvKYlo2C5XFo8Dg6\",\n \"type\": \"checkout.session.completed\",\n \"data\": {\n \"object\": {\n \"id\": \"cs_test_a1b2c3d4\",\n \"customer\": \"cus_Abc123\",\n \"payment_status\": \"paid\",\n \"metadata\": {\n \"council_id\": \"c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6\",\n \"sku\": \"P-25-2\"\n }\n }\n }\n}"
response = http.request(request)
puts response.read_bodyStripe webhook handler
Receives Stripe event webhooks and routes them to the appropriate
handler. Authentication is performed via HMAC-SHA256 signature
verification of the Stripe-Signature header against the stored
webhook secret (ss_stripe_webhook_secret).
Signature verification:
- Header format:
t={timestamp},v1={signature} - Signed payload:
{timestamp}.{raw_body} - Algorithm: HMAC-SHA256
- Tolerance: 300 seconds (5 minutes)
Handled event types:
checkout.session.completed— Creates council for new customers or adds credits for existinginvoice.paid— Handles renewal invoices, adds credits, fires renewal webhook, auto-creates renewal deal for E-SKUcharge.refunded— Deducts credits proportional to refund ratio (never below 0)customer.subscription.deleted— Marks council as cancelledcustomer.subscription.updated— Updates subscription SKUcheckout.session.async_payment_succeeded— Processes cleared async payments (ACH/bank)checkout.session.async_payment_failed— Cleans up failed async payment data
Unhandled event types are logged and acknowledged with {received: true}.
curl --request POST \
--url https://{domain}/wp-json/sagescreen/v1/stripe/webhook \
--header 'Content-Type: application/json' \
--header 'Stripe-Signature: <api-key>' \
--data '
{
"id": "evt_1NqIAB2eZvKYlo2C5XFo8Dg6",
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_test_a1b2c3d4",
"customer": "cus_Abc123",
"payment_status": "paid",
"metadata": {
"council_id": "c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6",
"sku": "P-25-2"
}
}
}
}
'import requests
url = "https://{domain}/wp-json/sagescreen/v1/stripe/webhook"
payload = {
"id": "evt_1NqIAB2eZvKYlo2C5XFo8Dg6",
"type": "checkout.session.completed",
"data": { "object": {
"id": "cs_test_a1b2c3d4",
"customer": "cus_Abc123",
"payment_status": "paid",
"metadata": {
"council_id": "c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6",
"sku": "P-25-2"
}
} }
}
headers = {
"Stripe-Signature": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Stripe-Signature': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
id: 'evt_1NqIAB2eZvKYlo2C5XFo8Dg6',
type: 'checkout.session.completed',
data: {
object: {
id: 'cs_test_a1b2c3d4',
customer: 'cus_Abc123',
payment_status: 'paid',
metadata: {council_id: 'c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6', sku: 'P-25-2'}
}
}
})
};
fetch('https://{domain}/wp-json/sagescreen/v1/stripe/webhook', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://{domain}/wp-json/sagescreen/v1/stripe/webhook",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'id' => 'evt_1NqIAB2eZvKYlo2C5XFo8Dg6',
'type' => 'checkout.session.completed',
'data' => [
'object' => [
'id' => 'cs_test_a1b2c3d4',
'customer' => 'cus_Abc123',
'payment_status' => 'paid',
'metadata' => [
'council_id' => 'c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6',
'sku' => 'P-25-2'
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Stripe-Signature: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://{domain}/wp-json/sagescreen/v1/stripe/webhook"
payload := strings.NewReader("{\n \"id\": \"evt_1NqIAB2eZvKYlo2C5XFo8Dg6\",\n \"type\": \"checkout.session.completed\",\n \"data\": {\n \"object\": {\n \"id\": \"cs_test_a1b2c3d4\",\n \"customer\": \"cus_Abc123\",\n \"payment_status\": \"paid\",\n \"metadata\": {\n \"council_id\": \"c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6\",\n \"sku\": \"P-25-2\"\n }\n }\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Stripe-Signature", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://{domain}/wp-json/sagescreen/v1/stripe/webhook")
.header("Stripe-Signature", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"id\": \"evt_1NqIAB2eZvKYlo2C5XFo8Dg6\",\n \"type\": \"checkout.session.completed\",\n \"data\": {\n \"object\": {\n \"id\": \"cs_test_a1b2c3d4\",\n \"customer\": \"cus_Abc123\",\n \"payment_status\": \"paid\",\n \"metadata\": {\n \"council_id\": \"c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6\",\n \"sku\": \"P-25-2\"\n }\n }\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://{domain}/wp-json/sagescreen/v1/stripe/webhook")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Stripe-Signature"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"id\": \"evt_1NqIAB2eZvKYlo2C5XFo8Dg6\",\n \"type\": \"checkout.session.completed\",\n \"data\": {\n \"object\": {\n \"id\": \"cs_test_a1b2c3d4\",\n \"customer\": \"cus_Abc123\",\n \"payment_status\": \"paid\",\n \"metadata\": {\n \"council_id\": \"c9d8e7f6-5a4b-3c2d-1e0f-a1b2c3d4e5f6\",\n \"sku\": \"P-25-2\"\n }\n }\n }\n}"
response = http.request(request)
puts response.read_bodyAuthorizations
Stripe webhook signature header. Format: t={unix_timestamp},v1={hmac_sha256_hex}.
Verified against ss_stripe_webhook_secret with a 5-minute timestamp tolerance.
Body
Stripe event JSON object. The full Stripe event envelope is passed
through; only type and data.object are consumed by the handler.
Stripe event ID
"evt_1NqIAB2eZvKYlo2C5XFo8Dg6"
Stripe event types handled by the webhook
checkout.session.completed, invoice.paid, charge.refunded, customer.subscription.deleted, customer.subscription.updated, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed "checkout.session.completed"
Show child attributes
Show child attributes
Response
Event received and processed
true