curl --request GET \
--url https://www.closient.com/integrations/api/v1/webhooks/events/ \
--header 'X-API-Key: <api-key>'import requests
url = "https://www.closient.com/integrations/api/v1/webhooks/events/"
headers = {"X-API-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<api-key>'}};
fetch('https://www.closient.com/integrations/api/v1/webhooks/events/', 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/events/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
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/events/"
req, _ := http.NewRequest("GET", 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.get("https://www.closient.com/integrations/api/v1/webhooks/events/")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/integrations/api/v1/webhooks/events/")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"data": [
{
"cursor": "2026-07-29T08:00:00+00:00~4t3rmMvzGQrqAqZ3vPPGmM",
"envelope": {
"cursor": "2026-07-29T08:00:00+00:00~4t3rmMvzGQrqAqZ3vPPGmM",
"data": {
"gtins": [
"00012345600012"
],
"recall_id": "..."
},
"id": "evt_9f0c2b1a4d6e4f0b8a1c2d3e4f5a6b7c",
"source": "closient",
"specversion": "2026-07-01",
"time": "2026-07-29T08:00:00+00:00",
"type": "recall.opened"
},
"event_id": "evt_9f0c2b1a4d6e4f0b8a1c2d3e4f5a6b7c",
"event_type": "recall.opened",
"occurred_at": "2026-07-29T08:00:00Z"
}
],
"pagination": {
"has_next": true,
"has_previous": false,
"page": 1,
"page_size": 25,
"total_count": 342,
"total_pages": 14
}
}{
"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"
}Replay events from a cursor (gap-fill on reconnect)
Walk your organization’s durable event log forward, oldest first. This is how a subscriber that was offline recovers: every event is logged when it is emitted, before any delivery is attempted, so an outage — yours or ours — leaves a record you can replay rather than a silent gap.
The loop is: persist the cursor of the last event you durably processed; on reconnect call this endpoint with ?after=<cursor>; process the page; repeat with the last cursor of that page until it comes back short. The walk is gap-free and duplicate-free even when two events share a timestamp, because the cursor is a keyset over (occurred_at, id) rather than a timestamp.
envelope is byte-identical to what a live delivery would have POSTed at that version, so replayed events need no special casing in your handler. event_id is stable across the live delivery and every replay — deduplicate on it.
curl --request GET \
--url https://www.closient.com/integrations/api/v1/webhooks/events/ \
--header 'X-API-Key: <api-key>'import requests
url = "https://www.closient.com/integrations/api/v1/webhooks/events/"
headers = {"X-API-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-Key': '<api-key>'}};
fetch('https://www.closient.com/integrations/api/v1/webhooks/events/', 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/events/",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
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/events/"
req, _ := http.NewRequest("GET", 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.get("https://www.closient.com/integrations/api/v1/webhooks/events/")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.closient.com/integrations/api/v1/webhooks/events/")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"data": [
{
"cursor": "2026-07-29T08:00:00+00:00~4t3rmMvzGQrqAqZ3vPPGmM",
"envelope": {
"cursor": "2026-07-29T08:00:00+00:00~4t3rmMvzGQrqAqZ3vPPGmM",
"data": {
"gtins": [
"00012345600012"
],
"recall_id": "..."
},
"id": "evt_9f0c2b1a4d6e4f0b8a1c2d3e4f5a6b7c",
"source": "closient",
"specversion": "2026-07-01",
"time": "2026-07-29T08:00:00+00:00",
"type": "recall.opened"
},
"event_id": "evt_9f0c2b1a4d6e4f0b8a1c2d3e4f5a6b7c",
"event_type": "recall.opened",
"occurred_at": "2026-07-29T08:00:00Z"
}
],
"pagination": {
"has_next": true,
"has_previous": false,
"page": 1,
"page_size": 25,
"total_count": 342,
"total_pages": 14
}
}{
"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
Query Parameters
Resume strictly after this cursor. Pass the cursor of the last event you durably processed — not the last one you received. Omit to start from the oldest retained event. An unrecognised cursor is a 422: silently restarting from the beginning would flood you, and silently starting from now would hide the very gap you are reconnecting to close.
Restrict to these event types. Repeat the parameter for multiple values. Omit for everything.
Every subscribable event type (C-4296).
Mirrors :class:apps.integrations.webhooks.catalog.WebhookEventType. The
two are asserted value-identical by
test_webhook_catalog.py::TestCatalogIntegrity — this is the mirror the
apps/CLAUDE.md API-empathy rule asks for, and typing the API's
event-type fields as this enum is what puts the catalog in the published
OpenAPI spec as enum: [...] instead of "any string".
Members are named after the wire value with dots and hyphens replaced, but the value is the wire string and is what everything round-trips on.
recall.opened, recall.amended, recall.expanded, recall.closed, signal.received, adverse_report.received, adverse_report.threshold_alert, serial_verification.flagged, test.ping, offer.updated, product.recalled, retailer.created Shorthand for the whole recall domain. Prefer this over listing the four recall.* values, because it keeps covering you when a fifth recall event ships.
Render each envelope at this version. Defaults to the version pinned on your endpoints, so a replayed event goes through the same handler as a live delivery.
v1, 2026-07-01 Events per page. Clamped to 200.
1 <= x <= 200Page number (1-indexed).
x >= 1Number of items per page (max 100).
1 <= x <= 100Response
OK
Items on the current page, each conforming to the endpoint's item schema. Empty when the result set is empty or page is past the end.
Show child attributes
Show child attributes
Pagination envelope describing position within the full result set.
Show child attributes
Show child attributes
{
"has_next": true,
"has_previous": false,
"page": 1,
"page_size": 25,
"total_count": 342,
"total_pages": 14
}