Webhook endpoints
Where signed webhooks go. Up to 10 endpoints per account, each with its own signing secret, shown once (on create and on rotate) and never returned again.
Where signed webhooks go. Up to 10 endpoints per account, each with its own signing secret, shown once (on create and on rotate) and never returned again.
Endpoints
Method | Path | Does |
|---|---|---|
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
|
How it works, when to use what, and what it costs: Webhooks. Base URL, authentication and errors: Overview.
GET /v1/webhook_endpoints
List webhook endpoints.
Every endpoint on the account, oldest first. Secrets are never returned: secret_hint shows the last four characters.
Request
- cURL
- Python
- Node.js
curl "https://api.scrapelyserp.com/v1/webhook_endpoints" \
-H "Authorization: Bearer $SCRAPELY_API_KEY"import os
import requests
HEADERS = {"Authorization": f"Bearer {os.environ['SCRAPELY_API_KEY']}"}
r = requests.get(
"https://api.scrapelyserp.com/v1/webhook_endpoints",
headers=HEADERS,
timeout=30,
)
r.raise_for_status()
data = r.json()const res = await fetch('https://api.scrapelyserp.com/v1/webhook_endpoints', {
headers: { Authorization: `Bearer ${process.env.SCRAPELY_API_KEY}` },
signal: AbortSignal.timeout(30_000),
});
if (!res.ok) throw new Error((await res.json()).error.code);
const data = await res.json();Response
{
"object": "list",
"data": [
{
"id": "we_7b1c2d3e4f5a6b7c8d9e",
"object": "webhook_endpoint",
"url": "https://example.com/hooks/scrapely",
"description": "Production",
"events": ["watchlist.product.changed", "watchlist.run.completed"],
"enabled": true,
"secret_hint": "whsec_…9c41",
"previous_secret_expires_at": null,
"created_at": "2026-10-06T08:00:00.000Z",
"updated_at": "2026-10-06T08:00:00.000Z"
}
],
"has_more": false
}Responses
Status | Meaning |
|---|---|
| Oldest first. No secrets. |
|
|
|
|
|
|
Response fields
Field | Type | Description |
|---|---|---|
| "list" | — |
| object[] | — |
| string | — |
| "webhook_endpoint" | — |
| string | Normalised. A watchlist's |
| string or null | — |
| string[] or null | Account-wide events this endpoint receives. |
| boolean | A disabled endpoint receives nothing; deliveries naming it are recorded as failed. |
| string | The last four characters of the current signing secret. The secret itself is never returned after create or rotate. |
| string (date-time) or null | During a rotation with overlap: when the previous secret stops signing (a second |
| string (date-time) | — |
| string (date-time) | — |
| boolean | — |
POST /v1/webhook_endpoints
Create a webhook endpoint.
Registers a URL and generates its own signing secret, returned in secret this once. At most 10 per account (409 beyond). Not idempotent by design: a replayable response would be a stored secret.
Request body
Name | Type | Required | Description |
|---|---|---|---|
| string | yes | HTTPS, a public host, port 443 or 1024–65535. Unique per account. |
| string or null | — | Up to 120 characters. |
| string[] or null | — | Account-wide events this endpoint receives. |
| boolean | — | Default |
Request
- cURL
- Python
- Node.js
curl -X POST "https://api.scrapelyserp.com/v1/webhook_endpoints" \
-H "Authorization: Bearer $SCRAPELY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/scrapely",
"description": "Production",
"events": ["watchlist.product.changed", "watchlist.run.completed"]
}'import os
import requests
HEADERS = {"Authorization": f"Bearer {os.environ['SCRAPELY_API_KEY']}"}
r = requests.post(
"https://api.scrapelyserp.com/v1/webhook_endpoints",
headers=HEADERS,
json={
"url": "https://example.com/hooks/scrapely",
"description": "Production",
"events": ["watchlist.product.changed", "watchlist.run.completed"],
},
timeout=30,
)
r.raise_for_status()
data = r.json()const res = await fetch('https://api.scrapelyserp.com/v1/webhook_endpoints', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SCRAPELY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://example.com/hooks/scrapely',
description: 'Production',
events: ['watchlist.product.changed', 'watchlist.run.completed'],
}),
signal: AbortSignal.timeout(30_000),
});
if (!res.ok) throw new Error((await res.json()).error.code);
const data = await res.json();Response
- 201 Created
- 400 Bad Request
{
"id": "we_7b1c2d3e4f5a6b7c8d9e",
"object": "webhook_endpoint",
"url": "https://example.com/hooks/scrapely",
"description": "Production",
"events": ["watchlist.product.changed", "watchlist.run.completed"],
"enabled": true,
"secret_hint": "whsec_…9c41",
"previous_secret_expires_at": null,
"created_at": "2026-10-06T08:00:00.000Z",
"updated_at": "2026-10-06T08:00:00.000Z",
"secret": "whsec_example_shown_once_store_it_now"
}{
"error": {
"code": "INVALID_PARAM",
"message": "`url` must be an absolute https:// URL.",
"param": "url",
"docs": "/docs/errors#invalid_param"
}
}Responses
Status | Meaning |
|---|---|
| Created. Save |
|
|
|
|
|
|
|
|
|
|
Response fields
Field | Type | Description |
|---|---|---|
| string | — |
| "webhook_endpoint" | — |
| string | Normalised. A watchlist's |
| string or null | — |
| string[] or null | Account-wide events this endpoint receives. |
| boolean | A disabled endpoint receives nothing; deliveries naming it are recorded as failed. |
| string | The last four characters of the current signing secret. The secret itself is never returned after create or rotate. |
| string (date-time) or null | During a rotation with overlap: when the previous secret stops signing (a second |
| string (date-time) | — |
| string (date-time) | — |
| string | The signing secret. Shown this once: store it now (it is kept encrypted and cannot be read back). |
GET /v1/webhook_endpoints/{id}
Get a webhook endpoint.
One endpoint, without its secret.
Path parameters
Name | Type | Required | Description |
|---|---|---|---|
| string | yes | The endpoint id ( |
Request
- cURL
- Python
- Node.js
curl "https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e" \
-H "Authorization: Bearer $SCRAPELY_API_KEY"import os
import requests
HEADERS = {"Authorization": f"Bearer {os.environ['SCRAPELY_API_KEY']}"}
r = requests.get(
"https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e",
headers=HEADERS,
timeout=30,
)
r.raise_for_status()
data = r.json()const res = await fetch('https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e', {
headers: { Authorization: `Bearer ${process.env.SCRAPELY_API_KEY}` },
signal: AbortSignal.timeout(30_000),
});
if (!res.ok) throw new Error((await res.json()).error.code);
const data = await res.json();Response
- 200 OK
- 404 Not Found
{
"id": "we_7b1c2d3e4f5a6b7c8d9e",
"object": "webhook_endpoint",
"url": "https://example.com/hooks/scrapely",
"description": "Production",
"events": ["watchlist.product.changed", "watchlist.run.completed"],
"enabled": true,
"secret_hint": "whsec_…9c41",
"previous_secret_expires_at": null,
"created_at": "2026-10-06T08:00:00.000Z",
"updated_at": "2026-10-06T08:00:00.000Z"
}{
"error": {
"code": "NOT_FOUND",
"message": "No webhook endpoint we_7b1c2d3e4f5a6b7c8d9e on this account.",
"param": "id",
"docs": "/docs/errors#not_found"
}
}Responses
Status | Meaning |
|---|---|
| The endpoint. No secret. |
|
|
|
|
|
|
|
|
Response fields
Field | Type | Description |
|---|---|---|
| string | — |
| "webhook_endpoint" | — |
| string | Normalised. A watchlist's |
| string or null | — |
| string[] or null | Account-wide events this endpoint receives. |
| boolean | A disabled endpoint receives nothing; deliveries naming it are recorded as failed. |
| string | The last four characters of the current signing secret. The secret itself is never returned after create or rotate. |
| string (date-time) or null | During a rotation with overlap: when the previous secret stops signing (a second |
| string (date-time) | — |
| string (date-time) | — |
PATCH /v1/webhook_endpoints/{id}
Update a webhook endpoint.
Change url, description, events or enabled. The secret is unchanged (rotate it separately).
Path parameters
Name | Type | Required | Description |
|---|---|---|---|
| string | yes | The endpoint id ( |
Request body
Name | Type | Required | Description |
|---|---|---|---|
| string | — | HTTPS, a public host, port 443 or 1024–65535. Unique per account. |
| string or null | — | Up to 120 characters. |
| string[] or null | — | Account-wide events this endpoint receives. |
| boolean | — | Default |
Request
- cURL
- Python
- Node.js
curl -X PATCH "https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e" \
-H "Authorization: Bearer $SCRAPELY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled":false}'import os
import requests
HEADERS = {"Authorization": f"Bearer {os.environ['SCRAPELY_API_KEY']}"}
r = requests.patch(
"https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e",
headers=HEADERS,
json={"enabled": False},
timeout=30,
)
r.raise_for_status()
data = r.json()const res = await fetch('https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.SCRAPELY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ enabled: false }),
signal: AbortSignal.timeout(30_000),
});
if (!res.ok) throw new Error((await res.json()).error.code);
const data = await res.json();Response
- 200 OK
- 404 Not Found
{
"id": "we_7b1c2d3e4f5a6b7c8d9e",
"object": "webhook_endpoint",
"url": "https://example.com/hooks/scrapely",
"description": "Production",
"events": ["watchlist.product.changed", "watchlist.run.completed"],
"enabled": false,
"secret_hint": "whsec_…9c41",
"previous_secret_expires_at": null,
"created_at": "2026-10-06T08:00:00.000Z",
"updated_at": "2026-10-16T09:15:04.120Z"
}{
"error": {
"code": "NOT_FOUND",
"message": "No webhook endpoint we_7b1c2d3e4f5a6b7c8d9e on this account.",
"param": "id",
"docs": "/docs/errors#not_found"
}
}Responses
Status | Meaning |
|---|---|
| Updated. |
|
|
|
|
|
|
|
|
|
|
|
|
Response fields
Field | Type | Description |
|---|---|---|
| string | — |
| "webhook_endpoint" | — |
| string | Normalised. A watchlist's |
| string or null | — |
| string[] or null | Account-wide events this endpoint receives. |
| boolean | A disabled endpoint receives nothing; deliveries naming it are recorded as failed. |
| string | The last four characters of the current signing secret. The secret itself is never returned after create or rotate. |
| string (date-time) or null | During a rotation with overlap: when the previous secret stops signing (a second |
| string (date-time) | — |
| string (date-time) | — |
DELETE /v1/webhook_endpoints/{id}
Delete a webhook endpoint.
Pending retries to it, and watchlists that name its URL, are recorded as failed deliveries from now on.
Path parameters
Name | Type | Required | Description |
|---|---|---|---|
| string | yes | The endpoint id ( |
Request
- cURL
- Python
- Node.js
curl -X DELETE "https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e" \
-H "Authorization: Bearer $SCRAPELY_API_KEY"import os
import requests
HEADERS = {"Authorization": f"Bearer {os.environ['SCRAPELY_API_KEY']}"}
r = requests.delete(
"https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e",
headers=HEADERS,
timeout=30,
)
r.raise_for_status()
data = r.json()const res = await fetch('https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e', {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.SCRAPELY_API_KEY}` },
signal: AbortSignal.timeout(30_000),
});
if (!res.ok) throw new Error((await res.json()).error.code);
const data = await res.json();Response
- 200 OK
- 404 Not Found
{
"id": "we_7b1c2d3e4f5a6b7c8d9e",
"object": "webhook_endpoint",
"deleted": true
}{
"error": {
"code": "NOT_FOUND",
"message": "No webhook endpoint we_7b1c2d3e4f5a6b7c8d9e on this account.",
"param": "id",
"docs": "/docs/errors#not_found"
}
}Responses
Status | Meaning |
|---|---|
| Deleted. |
|
|
|
|
|
|
|
|
Response fields
Field | Type | Description |
|---|---|---|
| string | — |
| "webhook_endpoint" | — |
| true | — |
POST /v1/webhook_endpoints/{id}/rotate
Rotate the signing secret.
A new secret, effective at once, returned in secret this once. With overlap: true (the default) the old secret keeps signing too — a second v1= in Scrapely-Signature — for 24 hours, so you can deploy the new one without missing a delivery. overlap: false drops it immediately (use it if the secret leaked).
Path parameters
Name | Type | Required | Description |
|---|---|---|---|
| string | yes | The endpoint id ( |
Request body (optional)
Name | Type | Required | Description |
|---|---|---|---|
| boolean | — | Default |
Request
- cURL
- Python
- Node.js
curl -X POST "https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e/rotate" \
-H "Authorization: Bearer $SCRAPELY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"overlap":true}'import os
import requests
HEADERS = {"Authorization": f"Bearer {os.environ['SCRAPELY_API_KEY']}"}
r = requests.post(
"https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e/rotate",
headers=HEADERS,
json={"overlap": True},
timeout=30,
)
r.raise_for_status()
data = r.json()const res = await fetch('https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e/rotate', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SCRAPELY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ overlap: true }),
signal: AbortSignal.timeout(30_000),
});
if (!res.ok) throw new Error((await res.json()).error.code);
const data = await res.json();Response
- 200 OK
- 404 Not Found
{
"id": "we_7b1c2d3e4f5a6b7c8d9e",
"object": "webhook_endpoint",
"url": "https://example.com/hooks/scrapely",
"description": "Production",
"events": ["watchlist.product.changed", "watchlist.run.completed"],
"enabled": true,
"secret_hint": "whsec_…77ab",
"previous_secret_expires_at": "2026-10-17T09:15:04.120Z",
"created_at": "2026-10-06T08:00:00.000Z",
"updated_at": "2026-10-16T09:15:04.120Z",
"secret": "whsec_example_shown_once_store_it_now"
}{
"error": {
"code": "NOT_FOUND",
"message": "No webhook endpoint we_7b1c2d3e4f5a6b7c8d9e on this account.",
"param": "id",
"docs": "/docs/errors#not_found"
}
}Responses
Status | Meaning |
|---|---|
| Rotated. Save |
|
|
|
|
|
|
|
|
|
|
Response fields
Field | Type | Description |
|---|---|---|
| string | — |
| "webhook_endpoint" | — |
| string | Normalised. A watchlist's |
| string or null | — |
| string[] or null | Account-wide events this endpoint receives. |
| boolean | A disabled endpoint receives nothing; deliveries naming it are recorded as failed. |
| string | The last four characters of the current signing secret. The secret itself is never returned after create or rotate. |
| string (date-time) or null | During a rotation with overlap: when the previous secret stops signing (a second |
| string (date-time) | — |
| string (date-time) | — |
| string | The signing secret. Shown this once: store it now (it is kept encrypted and cannot be read back). |
POST /v1/webhook_endpoints/{id}/test
Send a test event.
One signed webhook.test delivery to this endpoint, now, without retries. Recorded in the delivery log.
Path parameters
Name | Type | Required | Description |
|---|---|---|---|
| string | yes | The endpoint id ( |
Request
- cURL
- Python
- Node.js
curl -X POST "https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e/test" \
-H "Authorization: Bearer $SCRAPELY_API_KEY"import os
import requests
HEADERS = {"Authorization": f"Bearer {os.environ['SCRAPELY_API_KEY']}"}
r = requests.post(
"https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e/test",
headers=HEADERS,
timeout=30,
)
r.raise_for_status()
data = r.json()const res = await fetch('https://api.scrapelyserp.com/v1/webhook_endpoints/we_7b1c2d3e4f5a6b7c8d9e/test', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.SCRAPELY_API_KEY}` },
signal: AbortSignal.timeout(30_000),
});
if (!res.ok) throw new Error((await res.json()).error.code);
const data = await res.json();Response
- 200 OK
- 404 Not Found
{
"object": "webhook_test",
"endpoint_id": "we_7b1c2d3e4f5a6b7c8d9e",
"delivered": true,
"status_code": 200,
"duration_ms": 182,
"error": null
}{
"error": {
"code": "NOT_FOUND",
"message": "No webhook endpoint we_7b1c2d3e4f5a6b7c8d9e on this account.",
"param": "id",
"docs": "/docs/errors#not_found"
}
}Responses
Status | Meaning |
|---|---|
| What the endpoint answered. |
|
|
|
|
|
|
|
|
Response fields
Field | Type | Description |
|---|---|---|
| "webhook_test" | — |
| string | — |
| boolean | — |
| integer or null | — |
| integer | — |
| string or null | — |