Start here
Register an endpoint
POST
/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.
#Authentication
Authorization: Bearer auray_sk_… — webhooks:write.
#Request
#Body
application/json, required.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
url | string | yes | — | https only, 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. |
verification.status | integer | no | — | The HTTP status your endpoint answered with, if it answered. May be null. |
verification.response | string | no | — | The first bytes of what it said. 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.