curl --request POST \
--url https://api.scripe.io/v1/webhook-endpoints \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "Production CRM",
"url": "https://hooks.example.com/scripe",
"events": [
"post.created",
"job.completed"
],
"projectId": "proj_a1b2c3d4e5f6g7h8"
}
'import requests
url = "https://api.scripe.io/v1/webhook-endpoints"
payload = {
"name": "Production CRM",
"url": "https://hooks.example.com/scripe",
"events": ["post.created", "job.completed"],
"projectId": "proj_a1b2c3d4e5f6g7h8"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'Production CRM',
url: 'https://hooks.example.com/scripe',
events: ['post.created', 'job.completed'],
projectId: 'proj_a1b2c3d4e5f6g7h8'
})
};
fetch('https://api.scripe.io/v1/webhook-endpoints', 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://api.scripe.io/v1/webhook-endpoints",
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([
'name' => 'Production CRM',
'url' => 'https://hooks.example.com/scripe',
'events' => [
'post.created',
'job.completed'
],
'projectId' => 'proj_a1b2c3d4e5f6g7h8'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$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://api.scripe.io/v1/webhook-endpoints"
payload := strings.NewReader("{\n \"name\": \"Production CRM\",\n \"url\": \"https://hooks.example.com/scripe\",\n \"events\": [\n \"post.created\",\n \"job.completed\"\n ],\n \"projectId\": \"proj_a1b2c3d4e5f6g7h8\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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://api.scripe.io/v1/webhook-endpoints")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"Production CRM\",\n \"url\": \"https://hooks.example.com/scripe\",\n \"events\": [\n \"post.created\",\n \"job.completed\"\n ],\n \"projectId\": \"proj_a1b2c3d4e5f6g7h8\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.scripe.io/v1/webhook-endpoints")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"Production CRM\",\n \"url\": \"https://hooks.example.com/scripe\",\n \"events\": [\n \"post.created\",\n \"job.completed\"\n ],\n \"projectId\": \"proj_a1b2c3d4e5f6g7h8\"\n}"
response = http.request(request)
puts response.read_body{
"data": {
"id": "whe_a1b2c3d4e5f6g7h8",
"url": "https://hooks.example.com/scripe",
"name": "Production CRM",
"events": [
"post.created",
"job.completed"
],
"isActive": true,
"disabledReason": "<string>",
"projectId": "<string>",
"secretLast4": "<string>",
"createdAt": "2023-11-07T05:31:56Z",
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"updatedAt": "2023-11-07T05:31:56Z"
}
}{
"error": {
"code": "not_found",
"message": "<string>",
"request_id": "req_a1b2c3d4e5f6",
"docs_url": "<string>",
"details": "<unknown>"
}
}{
"error": {
"code": "not_found",
"message": "<string>",
"request_id": "req_a1b2c3d4e5f6",
"docs_url": "<string>",
"details": "<unknown>"
}
}{
"error": {
"code": "not_found",
"message": "<string>",
"request_id": "req_a1b2c3d4e5f6",
"docs_url": "<string>",
"details": "<unknown>"
}
}{
"error": {
"code": "not_found",
"message": "<string>",
"request_id": "req_a1b2c3d4e5f6",
"docs_url": "<string>",
"details": "<unknown>"
}
}Create webhook endpoint
Register a new outbound webhook endpoint. The response includes
the plaintext signing secret — store it immediately, the API
will never return it again. Subsequent reads expose only
secretLast4.
URL constraints:
- Must be HTTPS.
- Hostname must resolve to a public IP (loopback, link-local,
RFC 1918, and CGNAT ranges are rejected as
ssrf_blocked). - The resolved IP is pinned for ~24h to defeat DNS rebinding; re-resolution happens automatically and may auto-disable the endpoint if the IP starts pointing somewhere private.
Subscribe to one or more event names from the closed list (see
WebhookEventName schema). Subscribing to an unknown name
returns 400 invalid_request.
Requires the webhooks:manage scope.
curl --request POST \
--url https://api.scripe.io/v1/webhook-endpoints \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "Production CRM",
"url": "https://hooks.example.com/scripe",
"events": [
"post.created",
"job.completed"
],
"projectId": "proj_a1b2c3d4e5f6g7h8"
}
'import requests
url = "https://api.scripe.io/v1/webhook-endpoints"
payload = {
"name": "Production CRM",
"url": "https://hooks.example.com/scripe",
"events": ["post.created", "job.completed"],
"projectId": "proj_a1b2c3d4e5f6g7h8"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'Production CRM',
url: 'https://hooks.example.com/scripe',
events: ['post.created', 'job.completed'],
projectId: 'proj_a1b2c3d4e5f6g7h8'
})
};
fetch('https://api.scripe.io/v1/webhook-endpoints', 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://api.scripe.io/v1/webhook-endpoints",
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([
'name' => 'Production CRM',
'url' => 'https://hooks.example.com/scripe',
'events' => [
'post.created',
'job.completed'
],
'projectId' => 'proj_a1b2c3d4e5f6g7h8'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$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://api.scripe.io/v1/webhook-endpoints"
payload := strings.NewReader("{\n \"name\": \"Production CRM\",\n \"url\": \"https://hooks.example.com/scripe\",\n \"events\": [\n \"post.created\",\n \"job.completed\"\n ],\n \"projectId\": \"proj_a1b2c3d4e5f6g7h8\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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://api.scripe.io/v1/webhook-endpoints")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"Production CRM\",\n \"url\": \"https://hooks.example.com/scripe\",\n \"events\": [\n \"post.created\",\n \"job.completed\"\n ],\n \"projectId\": \"proj_a1b2c3d4e5f6g7h8\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.scripe.io/v1/webhook-endpoints")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"Production CRM\",\n \"url\": \"https://hooks.example.com/scripe\",\n \"events\": [\n \"post.created\",\n \"job.completed\"\n ],\n \"projectId\": \"proj_a1b2c3d4e5f6g7h8\"\n}"
response = http.request(request)
puts response.read_body{
"data": {
"id": "whe_a1b2c3d4e5f6g7h8",
"url": "https://hooks.example.com/scripe",
"name": "Production CRM",
"events": [
"post.created",
"job.completed"
],
"isActive": true,
"disabledReason": "<string>",
"projectId": "<string>",
"secretLast4": "<string>",
"createdAt": "2023-11-07T05:31:56Z",
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"updatedAt": "2023-11-07T05:31:56Z"
}
}{
"error": {
"code": "not_found",
"message": "<string>",
"request_id": "req_a1b2c3d4e5f6",
"docs_url": "<string>",
"details": "<unknown>"
}
}{
"error": {
"code": "not_found",
"message": "<string>",
"request_id": "req_a1b2c3d4e5f6",
"docs_url": "<string>",
"details": "<unknown>"
}
}{
"error": {
"code": "not_found",
"message": "<string>",
"request_id": "req_a1b2c3d4e5f6",
"docs_url": "<string>",
"details": "<unknown>"
}
}{
"error": {
"code": "not_found",
"message": "<string>",
"request_id": "req_a1b2c3d4e5f6",
"docs_url": "<string>",
"details": "<unknown>"
}
}Authorizations
Pass Authorization: Bearer scripe_sk_live_<...> (or
scripe_sk_test_<...> for test keys) on every request. Keys
are scoped to a single workspace and can be revoked from the
Scripe dashboard.
The same header also accepts an OAuth 2.1 access token
(scripe_oat_*); both credentials share one scope vocabulary
and every operation below documents the scope it requires.
An API key can hold every scope named on this surface except
webhooks:manage, which is grantable to OAuth tokens only
today — the webhook-endpoint operations answer
403 scope_missing to every API key. Operations that name no
scope accept any valid token of the workspace.
Headers
Pin the API version. Format YYYY-MM-DD. Omit to receive the
currently rolling default. Unknown versions return 400 version_unsupported.
"2026-08-10"
Opaque string (1–64 chars, [A-Za-z0-9_-]) used to dedup
retried writes. Within 24h of the first request, the same key
- same body returns the original response (
Idempotent-Replayed: true). Same key + different body returns409 idempotency_key_conflict.
Strongly recommended for every write — see
/docs/api/v1/idempotency.
^[A-Za-z0-9_-]{1,64}$Body
1 - 128"Production CRM"
1024"https://hooks.example.com/scripe"
1Closed list of subscribable events. Adding a new event name is a contract change — bump the docs + this enum in lockstep with the producer.
note.created, post.created, post.updated, post.scheduled, post.unscheduled, post.deleted, source.created, source.deleted, job.completed, job.failed, knowledge.indexed ["post.created", "job.completed"]
Optional project scope. When set, the endpoint only
receives events for that project. null (default)
delivers events for every project in the workspace.
"proj_a1b2c3d4e5f6g7h8"
Response
Endpoint created. Response carries the plaintext secret.
Display-safe shape of a webhook endpoint. The plaintext signing
secret is never present here — only secretLast4. Use
WebhookEndpointWithSecret (returned by create + rotate) when
the plaintext matters.
Show child attributes
Show child attributes