Technical documentation to integrate external systems with Auraltica via secure HTTP webhooks.
Auraltica Custom Webhooks let you send your orders, customers and products from any external system (ERP, custom platform, point-of-sale system, script) straight to your account. Once stored, the Auraltica chat can answer questions about them in natural language — with no need to connect a database.
Each endpoint has a unique URL, a public API Key and a Signing Secret to verify the authenticity of every request via HMAC-SHA256.
Prerequisites
Every request requires four headers. Missing any of them results in immediate rejection:
| Header | Description |
|---|---|
| Content-Type | Always application/json. |
| X-Auraltica-API-Key | Your public API Key (prefix awk_). Identifies the endpoint. 401 error if it doesn't match. |
| X-Auraltica-Timestamp | Unix timestamp in seconds of when you send the request. Auraltica rejects requests more than ±5 minutes off the server clock — this prevents replay attacks. 400 error if missing or expired. |
| X-Auraltica-HMAC-SHA256 | Base64 HMAC-SHA256 signature computed over timestamp + "." + body using the Signing Secret. Including the timestamp in the signature ensures it can't be reused. 401 error if it doesn't match. |
| Optional header | Description |
|---|---|
| X-Idempotency-Key | Unique UUID per event. If Auraltica already processed an event with the same key, it returns 200 without re-inserting data. Useful for safe retries from your system. |
The Signing Secret is only shown when you create or rotate the endpoint. Store it in an environment variable. Never include it in source code or send it in the request.
Make a POST to your endpoint URL with the four required headers:
POST https://<project>.supabase.co/functions/v1/custom-webhook-receiver/<endpoint_path>
Content-Type: application/json
X-Auraltica-API-Key: awk_abc123...
X-Auraltica-Timestamp: 1719316800
X-Auraltica-HMAC-SHA256: <base64_hmac>
X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 (optional)
{
"event_type": "order.created",
"data": {
"order_id": "ORD-001",
"customer": "John Doe",
"total": 1500.00,
"currency": "USD"
}
}The full endpoint URL is shown in the Connectors panel next to the API Key.
Possible responses
| Code | Cause | Recommended action |
|---|---|---|
| 200 | Event received and ingested successfully. | Nothing. Data already available in the chat. |
| 400 | Missing or expired timestamp (drift > 5 min), or invalid JSON. | Sync the server clock. Do not retry automatically. |
| 401 | Wrong API Key or invalid HMAC signature. | Check that the secret and the signing formula are correct. |
| 404 | Endpoint not found or disabled. | Check the URL in the Connectors panel. |
| 413 | Payload larger than 1 MB. | Split the payload into multiple requests. |
| 429 | Rate limit exceeded (N req/min configured on the endpoint). | Wait the time indicated in Retry-After (seconds). |
The signature is computed over timestamp + "." + body — the same principle Stripe uses. Including the timestamp in the signature ensures that an attacker who intercepts a valid request can't resend it later, because the timestamp will have expired.
signed_content = timestamp_string + "." + body_string
X-Auraltica-HMAC-SHA256 = Base64( HMAC-SHA256(signing_secret, signed_content) )Node.js
const crypto = require('crypto');
async function sendWebhook(endpointUrl, apiKey, signingSecret, eventType, data) {
const body = JSON.stringify({ event_type: eventType, data });
const timestamp = String(Math.floor(Date.now() / 1000)); // Unix seconds
const signedContent = timestamp + '.' + body;
const signature = crypto
.createHmac('sha256', signingSecret)
.update(signedContent)
.digest('base64');
const res = await fetch(endpointUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auraltica-API-Key': apiKey,
'X-Auraltica-Timestamp': timestamp,
'X-Auraltica-HMAC-SHA256': signature,
'X-Idempotency-Key': crypto.randomUUID(), // optional but recommended
},
body,
});
if (res.status === 429) {
const retryAfter = res.headers.get('Retry-After') ?? '60';
throw new Error(`Rate limit. Retry in ${retryAfter}s.`);
}
return res.json();
}
// Usage:
await sendWebhook(
process.env.AURALTICA_ENDPOINT_URL,
process.env.AURALTICA_API_KEY,
process.env.AURALTICA_SIGNING_SECRET,
'order.created',
{ order_id: 'ORD-001', total: 1500, currency: 'USD' }
);Python
import hmac, hashlib, base64, json, time, uuid, os
import requests
def send_webhook(endpoint_url, api_key, signing_secret, event_type, data):
body = json.dumps({"event_type": event_type, "data": data}, separators=(',', ':'))
timestamp = str(int(time.time()))
signed_content = timestamp + "." + body
signature = base64.b64encode(
hmac.new(signing_secret.encode(), signed_content.encode(), hashlib.sha256).digest()
).decode()
resp = requests.post(endpoint_url, data=body, headers={
"Content-Type": "application/json",
"X-Auraltica-API-Key": api_key,
"X-Auraltica-Timestamp": timestamp,
"X-Auraltica-HMAC-SHA256": signature,
"X-Idempotency-Key": str(uuid.uuid4()), # optional
})
if resp.status_code == 429:
retry_after = resp.headers.get("Retry-After", "60")
raise Exception(f"Rate limit. Retry in {retry_after}s.")
return resp.json()
# Usage:
send_webhook(
os.environ["AURALTICA_ENDPOINT_URL"],
os.environ["AURALTICA_API_KEY"],
os.environ["AURALTICA_SIGNING_SECRET"],
"order.created",
{"order_id": "ORD-001", "total": 1500, "currency": "USD"}
)PHP
<?php
function sendWebhook(string $url, string $apiKey, string $secret, string $eventType, array $data): array {
$body = json_encode(['event_type' => $eventType, 'data' => $data]);
$timestamp = (string) time();
$signed = $timestamp . '.' . $body;
$signature = base64_encode(hash_hmac('sha256', $signed, $secret, true));
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Auraltica-API-Key: ' . $apiKey,
'X-Auraltica-Timestamp: ' . $timestamp,
'X-Auraltica-HMAC-SHA256: ' . $signature,
'X-Idempotency-Key: ' . sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff),
mt_rand(0,0x0fff)|0x4000, mt_rand(0,0x3fff)|0x8000,
mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff)),
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 429) {
throw new RuntimeException('Rate limit exceeded. Check the Retry-After header.');
}
return json_decode($response, true);
}
// Usage:
sendWebhook(
$_ENV['AURALTICA_ENDPOINT_URL'],
$_ENV['AURALTICA_API_KEY'],
$_ENV['AURALTICA_SIGNING_SECRET'],
'order.created',
['order_id' => 'ORD-001', 'total' => 1500, 'currency' => 'USD']
);cURL / Bash
BODY='{"event_type":"order.created","data":{"order_id":"ORD-001","total":1500}}'
SECRET="aws_your_signing_secret"
API_KEY="awk_your_api_key"
TIMESTAMP=$(date +%s)
SIGNED="${TIMESTAMP}.${BODY}"
SIG=$(echo -n "$SIGNED" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)
curl -X POST "$ENDPOINT_URL" \
-H "Content-Type: application/json" \
-H "X-Auraltica-API-Key: $API_KEY" \
-H "X-Auraltica-Timestamp: $TIMESTAMP" \
-H "X-Auraltica-HMAC-SHA256: $SIG" \
-d "$BODY"Every request must include an event_type field that indicates the data type, and a data field with the content. Auraltica stores the data in your account so the chat can query it.
Supported events
order.created / order.updated / order.paid — store sales ordersorder.cancelled / order.refunded — update the order statuscustomer.created / customer.updated — store customersproduct.created / product.updated — store productsSales order
{
"event_type": "order.created",
"data": {
"order_id": "ORD-2024-001", // required, unique identifier
"date": "2026-06-25T10:00:00Z", // optional, defaults to now()
"customer": "John Doe", // customer name
"email": "john@email.com",
"phone": "+15551234567",
"total": 2499.00, // required
"subtotal": 2200.00,
"tax_amount": 299.00,
"currency": "USD", // defaults to "USD"
"financial_status": "paid", // paid | pending | refunded | cancelled
"fulfillment_status":"fulfilled", // fulfilled | unfulfilled | partial
"note": "Urgent order",
"items": [
{
"sku": "TSHIRT-BLUE-M",
"title": "Blue T-Shirt Size M",
"qty": 2,
"unit_price": 1100.00
},
{
"sku": "CAP-BLACK",
"title": "Black Cap",
"qty": 1,
"unit_price": 299.00
}
]
}
}Customer
{
"event_type": "customer.created",
"data": {
"customer_id": "CUST-001", // required, unique identifier
"first_name": "John",
"last_name": "Doe",
"email": "john@email.com",
"phone": "+15551234567",
"total_spent": 5000.00, // lifetime spend
"total_orders": 3, // number of orders
"city": "New York",
"state": "NY",
"country": "United States",
"zip": "10001",
"address": "123 Main St",
"note": "VIP customer"
}
}Product
{
"event_type": "product.created",
"data": {
"product_id": "PROD-001", // required, unique identifier
"title": "Blue T-Shirt",
"description": "100% cotton t-shirt, available in several sizes",
"vendor": "My Brand",
"product_type": "Apparel",
"status": "active", // active | inactive | archived
"sku": "TSHIRT-BLUE"
}
}You can review the history of received events directly from the panel:
A rejected status means the API Key or the HMAC signature didn't match what's registered in the system.
| Control | Value / Behavior |
|---|---|
| Max body size | 1 MB — 413 error before processing |
| Rate limit | Configurable per endpoint (default 60 req/min). 429 error with Retry-After and X-RateLimit-Reset headers. |
| Timestamp window | ±5 minutes. Requests outside the window are rejected (400) to prevent replay attacks. |
| Signing algorithm | HMAC-SHA256(secret, timestamp + "." + body) → Base64 |
| Idempotency | Optional X-Idempotency-Key header. A second attempt with the same key returns 200 without duplicating data. |
| Secret storage | 256-bit AES-GCM at rest. Only shown in plaintext when created or rotated. |
| Transport | HTTPS required (endpoint on Supabase Edge Functions). |
| Function timeout | ~30 s (standard Supabase Edge Functions limit) |
Best practices
AURALTICA_SIGNING_SECRET in environment variables or a vault — never in source code.X-Idempotency-Key when sending retries to avoid duplicating data.Retry-After header.