Start here
Register an endpoint
/v1/webhookson https://api.auray.ai/v1Sends one signed challenge to the URL.
Sends one signed challenge to the URL. The endpoint switches on only if it answers 200 with the nonce we sent — which is why registering somebody else's URL cannot make us send them anything. The challenge names the registering account in account, inside what the signature covers, so a receiver echoes the nonce only for its own account. 201 even when the challenge fails: the endpoint exists, it is simply not switched on. Registering a URL you already have resets that endpoint rather than adding one — it is pending until it answers the new challenge — and the plan's limit on endpoints (limits.api.webhooks_max in GET /plans) is counted over your other endpoints, so re-registering the one endpoint a plan holds is never refused 409 too_many_webhooks.
#Authentication
Authorization: Bearer auray_sk_… — webhooks:write.
#Request
#Body
application/json, required.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
url | string | yes | — | https only, on the standard port (443), public host, no credentials. A URL. |
products | string[] | no | — | Empty or omitted means every product. |
#Example request
curl -X POST https://api.auray.ai/v1/webhooks \
-H "Authorization: Bearer $AURAY_API_KEY" \
-H "content-type: application/json" \
-d '{
"url": "https://example.com/hooks/auray"
}'import { createClient } from "@auray/client";
const auray = createClient(); // reads AURAY_API_KEY
const webhook = await auray.platform.webhooks.create({ url: "https://example.com/hooks/auray" });
console.log(webhook);from auray_client import Client
auray = Client() # reads AURAY_API_KEY
webhook = auray.platform.webhooks.create(url="https://example.com/hooks/auray")
print(webhook)const res = await fetch("https://api.auray.ai/v1/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AURAY_API_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
"url": "https://example.com/hooks/auray"
}),
});
console.log(await res.json());import os, requests
r = requests.post(
"https://api.auray.ai/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['AURAY_API_KEY']}"},
json={
"url": "https://example.com/hooks/auray"
},
)
r.raise_for_status()
print(r.json())package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)
func main() {
body := []byte(`{
"url": "https://example.com/hooks/auray"
}`)
req, err := http.NewRequest("POST", "https://api.auray.ai/v1/webhooks", bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("AURAY_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}<?php
$ch = curl_init("https://api.auray.ai/v1/webhooks");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("AURAY_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"url" => "https://example.com/hooks/auray",
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response, PHP_EOL;require "net/http"
require "json"
uri = URI("https://api.auray.ai/v1/webhooks")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch("AURAY_API_KEY")}"
req["Content-Type"] = "application/json"
req.body = {
"url" => "https://example.com/hooks/auray"
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code, res.bodyimport java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
String body = """
{
"url": "https://example.com/hooks/auray"
}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.auray.ai/v1/webhooks"))
.header("Authorization", "Bearer " + System.getenv("AURAY_API_KEY"))
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}using System.Net.Http.Headers;
using System.Text;
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AURAY_API_KEY"));
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.auray.ai/v1/webhooks");
var body = """
{
"url": "https://example.com/hooks/auray"
}
""";
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");#Responses
#201 Created
Registered.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | no | — | |
object | string | no | — | One of "webhook". |
url | string | no | — | A URL. |
products | string[] | no | — | |
status | string | no | — | active if the challenge succeeded just now, pending if it did not. One of "pending", "active". |
verified | boolean | no | — | |
verify_keys | string | no | — | Where the public keys that sign your deliveries are published. Fetch it, cache it, and look up the kid each Auray-Signature names. A URL rather than a key, because a copy handed back here would go stale the moment one is rotated — and free rotation is the point of the scheme. A URL. |
signature_algorithm | string | no | — | How Auray-Signature is signed: Ed25519 over <t>.<raw body>, the header reading t=…,v2=…,kid=…. THERE IS NO SHARED SECRET — this response used to return secret (whsec_…) for an HMAC scheme, and the same bytes that verified a delivery could forge one. The private half of this key never leaves the deployment. One key signs for every account, so the body names who it is for, inside what the signature covers: account (your account id, the id GET /v1/me returns) and webhook_id (the endpoint, or null for a per-request ?webhook). Compare account with yours and refuse anything else. One of "EdDSA". |
created_at | string | no | — | |
verification | object | no | — | Present ONLY when the challenge failed — and the response is still a success, because the endpoint was registered; it is your server that did not answer. |
verification.failed | string | no | — | Which check failed, in our words: outbound_paused (deliveries are paused on our side; nothing was sent), host_paused (deliveries to this host are paused for now; nothing was sent), challenge_not_stored (nothing was sent; try again), private_address (the name resolves to an address we never send to), unreachable (no answer: DNS, connection, TLS or timeout), refused (it answered, not 2xx — status says what), nonce_not_echoed (it answered 2xx without the nonce), activation_failed (it echoed, and the endpoint could not be switched on; try again). What your server said is never quoted back. One of "outbound_paused", "host_paused", "challenge_not_stored", "private_address", "unreachable", "refused", "nonce_not_echoed", "activation_failed". |
verification.status | integer | no | — | The HTTP status your endpoint answered with, if it answered. May be null. |
verification.how | string | no | — | What to do about it, including the id to retry against. |
request_id | string | no | — | Mirrored in the X-Request-Id header. |
Headers:
| Header | Description |
|---|---|
RateLimit-Limit | Burst capacity. |
RateLimit-Remaining | |
RateLimit-Reset | Seconds until refill. |
X-RateLimit-Limit | |
X-RateLimit-Remaining | |
X-RateLimit-Reset | Unix seconds. The X- spelling is what client libraries read. |
X-Request-Id |
#400 Bad Request
#401 Unauthorized
#403 Forbidden
#409 Conflict
#413 Payload Too Large
#429 Too Many Requests
#500 Internal Server Error
#503 Service Unavailable
#Status codes
| Status | Meaning |
|---|---|
201 | Registered. |
400 | Bad Request |
401 | Unauthorized |
403 | Forbidden |
409 | Conflict |
413 | Payload Too Large |
429 | Too Many Requests |
500 | Internal Server Error |
503 | Service Unavailable |
Every refusal names its code, and every code is explained at Errors.