curl --request POST \
--url https://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/ \
--header 'X-API-Key: <api-key>'import requests
url = "https://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/"
headers = {"X-API-Key": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'X-API-Key': '<api-key>'}};
fetch('https://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/', 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://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"X-API-Key: <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"
"net/http"
"io"
)
func main() {
url := "https://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"allowed_rate": 120,
"api_version": "cloudevents-1.0",
"created_at": "2025-06-15T10:00:00Z",
"description": "Production webhook endpoint",
"event_types": [
"recall",
"adverse_report.threshold_alert"
],
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"is_active": true,
"metadata": {
"order_id": "6735"
},
"signing_secret": "whsec_12...",
"state": "active",
"updated_at": "2025-08-01T14:30:00Z",
"url": "https://example.com/webhooks/closient",
"verification_error": "",
"verified_at": "2025-06-15T10:00:02Z"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}Verify endpoint ownership
Re-run the CloudEvents abuse-protection handshake against this endpoint’s URL and activate it if the destination consents. This is the way out of state: pending_verification — the state every endpoint is created in, and returns to whenever its url changes.
Closient sends OPTIONS to your URL with:
WebHook-Request-Origin— the Closient origin asking for consentWebHook-Request-Rate— the most requests per minute we may send
Answer 200 with WebHook-Allowed-Origin set to that origin (or *), and optionally WebHook-Allowed-Rate. Anything else leaves the endpoint inactive and returns 422 with the reason, which is also recorded on verification_error. Nothing is delivered to an endpoint that has not completed this handshake — registering a URL is not evidence that its owner wants our traffic.
Spec: https://github.com/cloudevents/spec/blob/main/cloudevents/http-webhook.md
curl --request POST \
--url https://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/ \
--header 'X-API-Key: <api-key>'import requests
url = "https://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/"
headers = {"X-API-Key": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'X-API-Key': '<api-key>'}};
fetch('https://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/', 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://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"X-API-Key: <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"
"net/http"
"io"
)
func main() {
url := "https://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/integrations/api/v1/webhooks/endpoints/{endpoint_id}/verify/")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"allowed_rate": 120,
"api_version": "cloudevents-1.0",
"created_at": "2025-06-15T10:00:00Z",
"description": "Production webhook endpoint",
"event_types": [
"recall",
"adverse_report.threshold_alert"
],
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"is_active": true,
"metadata": {
"order_id": "6735"
},
"signing_secret": "whsec_12...",
"state": "active",
"updated_at": "2025-08-01T14:30:00Z",
"url": "https://example.com/webhooks/closient",
"verification_error": "",
"verified_at": "2025-06-15T10:00:02Z"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}{
"type": "https://closient.com/docs/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "The requested resource was not found.",
"error_code": "not_found",
"retryable": false,
"timestamp": "2026-03-31T12:00:00+00:00"
}Authorizations
Path Parameters
UUID of the webhook endpoint. Returned as the id field on every endpoint response.
22^[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{22}$Response
OK
A webhook endpoint as read from the API.
Returned by GET /webhooks/endpoints/{endpoint_id}/, the list view,
and the rotate-secret/test endpoints. signing_secret is masked
except on the initial create response and on the rotate-secret
response — see :data:_SIGNING_SECRET_READ_DESC.
UUID identifier of the endpoint. Use this in path params and the deliveries-list endpoint_id filter.
22^[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{22}$Target URL the webhook payload will be POSTed to. HTTPS only, in every environment — there is no setting, environment variable or local-development exemption that accepts http://. The URL must not carry credentials (https://user:pass@host/), and its hostname must resolve exclusively to globally routable addresses: private, loopback, link-local, multicast, reserved and cloud-metadata ranges are rejected for both IPv4 and IPv6, at registration and again before every delivery. A rejection comes back as 422 with the reason on url in field_errors. Maximum 2048 characters. The request body is the raw event JSON; the signature lives in the signature header (see signing_secret for the verification recipe).
2048HMAC-SHA256 signing secret for verifying the X-Closient-Signature header on inbound deliveries. Returned in full only on creation and rotation; subsequent GET/LIST responses return a masked form (<first-8-chars>...). Verification recipe: compute HMAC-SHA256(secret, f"{timestamp}.{raw_body}") where timestamp is the t= portion of the header, then hmac.compare_digest against the v1= portion. Reject if the timestamp is older than 5 minutes (replay protection). During a 24-hour rotation grace period the header carries an additional v1old= signature using the prior secret — accept either.
Whether this endpoint should receive deliveries. false pauses delivery without deleting historical records or losing the signing secret — useful while debugging a customer-side outage. New events are not enqueued for inactive endpoints. On create, true is a request to activate, not a guarantee: the endpoint is created in pending_verification and becomes active only once your URL answers the ownership handshake (see state). Setting true on an unverified endpoint re-runs that handshake rather than switching it on.
Envelope version this endpoint receives. Pinned when the endpoint is created and never moved by Closient — shipping a new envelope version does not change what an existing endpoint gets, so an integration can keep running untouched indefinitely. PATCH this field when your parser is ready for a newer shape. This field also selects the signing scheme, so an endpoint is never half-migrated. v1 is the pre-catalog envelope (event_type / version / event_id / timestamp / data); 2026-07-01 is CloudEvents-shaped (type / specversion / id / time / source / data) and adds the cursor used for replay-on-reconnect; both are signed with the legacy X-Closient-Signature header. cloudevents-1.0 is the current version — a spec-conformant CloudEvents 1.0 structured-mode envelope with GDTI / record-version / jurisdiction routing extension attributes, signed with Standard Webhooks (webhook-id / webhook-timestamp / webhook-signature).
v1, 2026-07-01, cloudevents-1.0 Derived lifecycle state. active mirrors is_active=true; pending_verification is a newly registered (or newly re-pointed) endpoint that has not yet answered the ownership handshake and therefore receives nothing — retry it with POST .../verify/; paused is a self-serve, resumable pause (POST .../pause/ / .../resume/); disabled covers both a deletion and an automatic disable after a sustained 100% delivery failure rate — neither is resumable through the resume action. Read-only: set indirectly via is_active or the pause/resume/verify actions, never written directly.
active, pending_verification, paused, disabled Server-side ISO 8601 timestamp (UTC) of when the resource was first persisted.
Server-side ISO 8601 timestamp (UTC) of the resource's last modification.
Developer-attached key/value data attached to this object. Up to 50 keys; key max 40 chars, value max 500 chars.
Show child attributes
Show child attributes
Optional human-readable label for this endpoint. Surfaced in the brand portal's webhooks list alongside the URL — useful when a single account has separate staging / production endpoints. Maximum 255 characters. Has no effect on delivery.
255Event types this endpoint subscribes to, drawn from the catalog at GET /webhooks/event-types/. Two forms are accepted: a full wire value (recall.opened) or a domain prefix (recall), which subscribes to every event in that domain including ones added after you subscribe — the recommended form for safety-critical domains. Values outside the catalog are rejected with 422 rather than silently accepted, so a typo cannot leave you subscribed to nothing. An empty list means every event type (fail-open); use is_active=false to pause delivery instead.
When this destination last passed the CloudEvents abuse-protection handshake. null means it never has, and the endpoint cannot become active. Reset to null whenever url changes — consent is per destination, not per subscription.
Why the last verification attempt failed, verbatim (e.g. a missing WebHook-Allowed-Origin header, a non-2xx answer, or a destination that failed address policy). Empty string when the endpoint is verified.
255Requests per minute this destination stated it accepts, from the handshake's WebHook-Allowed-Rate header. null means it stated no limit. Recorded for transparency; delivery pacing is not yet enforced against it.
x >= 1Deliver only events that reference at least one of these GTINs. Accepts GTIN-8/12/13/14 in any digit-count and normalizes to GTIN-14 (apps.core.types.gtin.GTIN). Backed by an indexed lookup table, so this scales to very large lists (100,000+ GTINs) without a per-event table scan. Empty list = no restriction (all GTINs).
Deliver only events whose product GTIN's company-prefix segment is one of these GS1 Company Prefixes (4-12 digits). Empty list = no restriction (all prefixes). Note: matched against the GS1-13/14 numbering alignment; a GTIN whose native form is UPC-A (GTIN-12) may not match its equivalent U.P.C. Company Prefix here — use the gtins filter for exact control in that case.
Deliver only events tagged with one of these ISO 3166-1 alpha-2 jurisdiction codes (e.g. US, CA). Empty list = no restriction (all jurisdictions).
Deliver only events for products in one of these 8-digit GS1 Global Product Classification brick codes. Empty list = no restriction (all categories).
Deliver only recall events whose recall is currently in one of these lifecycle statuses. Empty list = no restriction (all statuses).
A recall's lifecycle status, for the lifecycle_statuses filter dimension (C-4812).
Mirrors :class:apps.compliance.models.RecallEvent.LifecycleStatus.
PUBLISHED, AMENDED, CLOSED, RETRACTED