Create Webhook
curl --request POST \
--url https://api.shoal.xyz/v1/webhooks \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.shoal.xyz/v1/webhooks"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.shoal.xyz/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.shoal.xyz/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_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$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://api.shoal.xyz/v1/webhooks"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
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.shoal.xyz/v1/webhooks")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.shoal.xyz/v1/webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_bodyWebhooks
Create Webhook
Register a new webhook to receive real-time event notifications
POST
/
v1
/
webhooks
Create Webhook
curl --request POST \
--url https://api.shoal.xyz/v1/webhooks \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.shoal.xyz/v1/webhooks"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.shoal.xyz/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.shoal.xyz/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_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$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://api.shoal.xyz/v1/webhooks"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
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.shoal.xyz/v1/webhooks")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.shoal.xyz/v1/webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_bodyRegister a webhook URL to receive
radar and/or signal events as they occur. Shoal will POST a JSON payload to your URL whenever a matching event is detected.
Webhook creation is available only on plans that include webhook access.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS endpoint to receive events. Must not point to a private/internal address. |
event_types | string[] | Yes | Array of event types to subscribe to: radar, signal, or both. |
Request
cURL
curl -X POST "https://api.shoal.xyz/v1/webhooks" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/shoal-webhook",
"event_types": ["radar", "signal"]
}'
Python
import os, requests
API_KEY = os.environ.get("SHOAL_API_KEY", "YOUR_API_KEY")
r = requests.post(
"https://api.shoal.xyz/v1/webhooks",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={
"url": "https://example.com/shoal-webhook",
"event_types": ["radar", "signal"],
},
)
print(r.json())
JavaScript
const API_KEY = process.env.SHOAL_API_KEY || 'YOUR_API_KEY';
const res = await fetch('https://api.shoal.xyz/v1/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://example.com/shoal-webhook',
event_types: ['radar', 'signal'],
}),
});
console.log(await res.json());
Response (201)
{
"data": {
"id": 12,
"url": "https://example.com/shoal-webhook",
"secret": "a1b2c3d4e5f6...64-char-hex-string",
"event_types": ["radar", "signal"],
"active": true,
"created_at": "2026-03-10T12:00:00Z"
}
}
Save the
secret from the response — it is only returned once at creation time. You’ll need it to verify webhook signatures. See Webhook Signatures.Errors
| Status | Error | Cause |
|---|---|---|
| 400 | url is required | Missing URL |
| 400 | URL must use HTTPS | Non-HTTPS URL |
| 400 | URL must not point to a private address | URL resolves to localhost/private IP |
| 400 | event_types must be a non-empty array | Missing or empty event types |
| 400 | event_types must only contain: radar, signal | Invalid event type |
| 400 | Maximum 5 webhooks allowed | Per-account webhook limit reached |
⌘I