Webhooks
Create Webhook
Register a new webhook endpoint to receive real-time event notifications.
POST
/
api
/
v1
/
webhooks
Create Webhook
curl --request POST \
--url https://api.example.com/api/v1/webhooks \
--header 'Content-Type: application/json' \
--data '
{
"url": "<string>",
"event_types": [
"<string>"
],
"secret": "<string>",
"description": "<string>"
}
'import requests
url = "https://api.example.com/api/v1/webhooks"
payload = {
"url": "<string>",
"event_types": ["<string>"],
"secret": "<string>",
"description": "<string>"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
url: '<string>',
event_types: ['<string>'],
secret: '<string>',
description: '<string>'
})
};
fetch('https://api.example.com/api/v1/webhooks', 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.example.com/api/v1/webhooks",
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([
'url' => '<string>',
'event_types' => [
'<string>'
],
'secret' => '<string>',
'description' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"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.example.com/api/v1/webhooks"
payload := strings.NewReader("{\n \"url\": \"<string>\",\n \"event_types\": [\n \"<string>\"\n ],\n \"secret\": \"<string>\",\n \"description\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
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.example.com/api/v1/webhooks")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"<string>\",\n \"event_types\": [\n \"<string>\"\n ],\n \"secret\": \"<string>\",\n \"description\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"<string>\",\n \"event_types\": [\n \"<string>\"\n ],\n \"secret\": \"<string>\",\n \"description\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"url": "<string>",
"event_types": [
"<string>"
],
"enabled": true,
"signing_secret": "<string>",
"created_at": "<string>"
}Request
string
required
The HTTPS URL to deliver events to. Must use
https:// in production. Subject to SSRF validation — private IPs, cloud metadata endpoints, and non-HTTP schemes are blocked.string[]
required
Array of event types to subscribe to. Use
"*" for all events, or specific types like "completion.created".Valid types: completion.created, job.completed, job.failed, workflow.completed, workflow.failed, skill.installed, skill.uninstalled, skill.submitted, skill.approved, skill.rejected, skill.suspended, connector.created, connector.status_changed, credits.low, key.created, key.revoked, *string
Optional custom signing secret (16–256 chars). If omitted, a cryptographically random 32-byte hex secret is generated.
string
Optional description for the webhook (max 256 chars).
Response
string
The webhook’s unique UUID.
string
The registered endpoint URL.
string[]
Subscribed event types.
boolean
Always
true on creation.string
The HMAC-SHA256 signing secret. Shown once — save it immediately. Used to verify webhook signatures on your server.
string
ISO 8601 timestamp.
Authentication
Requires an org-scoped API key withcompletions scope.
Authorization: Bearer theo_sk_...
Example
curl -X POST https://www.hitheo.ai/api/v1/webhooks \
-H "Authorization: Bearer $THEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/theo",
"event_types": ["completion.created", "job.completed"]
}'
import Theo from "@hitheo/sdk";
const theo = new Theo({ apiKey: process.env.THEO_API_KEY });
const webhook = await theo.createWebhook({
url: "https://your-server.com/webhooks/theo",
event_types: ["completion.created", "job.completed"],
});
// Save webhook.signing_secret — shown once only
console.log("Signing secret:", webhook.signing_secret);
Response
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"url": "https://your-server.com/webhooks/theo",
"event_types": ["completion.created", "job.completed"],
"enabled": true,
"signing_secret": "8f3a1b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1",
"created_at": "2026-04-15T12:00:00.000Z",
"_note": "Save the signing_secret — it will not be shown again."
}
Limits
- A per-organization cap on active webhooks applies. If you need more, contact support.
- URL must pass SSRF validation (no private IPs, no cloud metadata endpoints).
- HTTPS required in production.
Signature Verification
Every delivery includes three headers for verification:Theo-Webhook-Signature: v1=<hex>
Theo-Webhook-Timestamp: <unix_seconds>
Theo-Webhook-Id: <event_id>
HMAC-SHA256(signing_secret, timestamp + "." + raw_body) and comparing to the signature. Reject if the timestamp drifts more than 5 minutes from your clock.
const crypto = require("crypto");
function verifyWebhook(rawBody, headers, secret) {
const timestamp = headers["theo-webhook-timestamp"];
const signature = headers["theo-webhook-signature"];
// Replay protection
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (age > 300) throw new Error("Timestamp too old");
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const [, hash] = signature.split("=");
if (hash !== expected) throw new Error("Invalid signature");
return JSON.parse(rawBody);
}
Was this page helpful?
Create Webhook
curl --request POST \
--url https://api.example.com/api/v1/webhooks \
--header 'Content-Type: application/json' \
--data '
{
"url": "<string>",
"event_types": [
"<string>"
],
"secret": "<string>",
"description": "<string>"
}
'import requests
url = "https://api.example.com/api/v1/webhooks"
payload = {
"url": "<string>",
"event_types": ["<string>"],
"secret": "<string>",
"description": "<string>"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
url: '<string>',
event_types: ['<string>'],
secret: '<string>',
description: '<string>'
})
};
fetch('https://api.example.com/api/v1/webhooks', 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.example.com/api/v1/webhooks",
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([
'url' => '<string>',
'event_types' => [
'<string>'
],
'secret' => '<string>',
'description' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"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.example.com/api/v1/webhooks"
payload := strings.NewReader("{\n \"url\": \"<string>\",\n \"event_types\": [\n \"<string>\"\n ],\n \"secret\": \"<string>\",\n \"description\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
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.example.com/api/v1/webhooks")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"<string>\",\n \"event_types\": [\n \"<string>\"\n ],\n \"secret\": \"<string>\",\n \"description\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"<string>\",\n \"event_types\": [\n \"<string>\"\n ],\n \"secret\": \"<string>\",\n \"description\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"url": "<string>",
"event_types": [
"<string>"
],
"enabled": true,
"signing_secret": "<string>",
"created_at": "<string>"
}