Integrations
Diazluna connects to your systems two ways: outbound webhooks (we send you events when things happen) and tools for María (the AI queries your data live during a call). This page covers the technical contract for both.
Outbound webhooks
When something happens in Diazluna (a call gets captured, your content is published, your tier hits a threshold), we send a signed POST to whatever URL you register in the admin console under Integrations → Webhooks.
Request shape
POST https://your-domain.com/diazluna-webhook
Content-Type: application/json
X-Diazluna-Signature: t=1778302100,v1=a1b2c3d4...
User-Agent: Diazluna-Webhook/1
{
"event_id": "9f8e2dc8-0cc0-9bf6-...",
"event_type": "call.captured",
"occurred_at": "2026-05-09T13:45:00Z",
"customer_id": "11111111-2222-3333-4444-555555555555",
"data": { ... }
} Retries
If your endpoint returns a non-2xx status (or doesn't respond within 30 seconds),
we retry with exponential backoff: 1s → 5s → 30s → 5min → 30min.
After 6 consecutive failures we mark the delivery dead and stop
retrying. You can manually re-queue from the admin panel.
Idempotency
Each event has a stable event_id that survives across retries. If your
system has already processed an event_id, drop it and respond 200
(no need to re-process).
HMAC-SHA256 signing scheme
The X-Diazluna-Signature header follows the same format Stripe uses:
X-Diazluna-Signature: t=<unix_seconds>,v1=<hmac_hex>
where:
t = unix timestamp at the moment we signed
v1 = HMAC_SHA256(your_secret, "<t>.<raw_body>") To validate:
- Read the
X-Diazluna-Signatureheader. - Split on
,to extracttandv1. -
Verify
tis within the last 5 minutes (replay protection). -
Compute
HMAC_SHA256(secret, t + "." + raw_body_string)in hex. -
Compare with
v1using constant-time comparison (prevents timing attacks). Your language likely has a dedicated function:crypto.timingSafeEqualin Node,hmac.compare_digestin Python,hash_equalsin PHP.
The secret is shown to you exactly once when you create the subscription (or rotate). Store it as an environment variable like any other credential. If lost, rotate from the admin panel.
Verifying the signature
Copy-paste snippets:
Node.js (no dependencies)
import crypto from "node:crypto";
export function verifyDiazlunaSignature(opts) {
const { rawBody, headerValue, secret } = opts;
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(headerValue || "");
if (!m) return false;
const ts = Number(m[1]);
const sig = m[2];
// 5-minute replay window
if (Math.abs(Math.floor(Date.now() / 1000) - ts) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(\`\${ts}.\${rawBody}\`)
.digest("hex");
// Constant-time compare
try {
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(sig, "hex"),
);
} catch {
return false;
}
}
// In your route handler:
// const rawBody = req.rawBody.toString("utf8"); // capture before JSON.parse
// const sigHeader = req.headers["x-diazluna-signature"];
// if (!verifyDiazlunaSignature({ rawBody, headerValue: sigHeader, secret: process.env.DIAZLUNA_WEBHOOK_SECRET })) {
// return res.status(401).end();
// } Python 3 (no dependencies)
import hmac
import hashlib
import re
import time
SIG_RE = re.compile(r"^t=(\d+),v1=([0-9a-f]{64})$")
def verify_diazluna_signature(raw_body: bytes, header_value: str, secret: str) -> bool:
m = SIG_RE.match(header_value or "")
if not m:
return False
ts = int(m.group(1))
sig_hex = m.group(2)
if abs(int(time.time()) - ts) > 300:
return False
expected = hmac.new(
secret.encode("utf-8"),
f"{ts}.{raw_body.decode('utf-8')}".encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, sig_hex)
# In Flask, for example:
# raw_body = request.get_data() # bytes — DO NOT use request.json
# sig_header = request.headers.get("X-Diazluna-Signature", "")
# if not verify_diazluna_signature(raw_body, sig_header, os.environ["DIAZLUNA_WEBHOOK_SECRET"]):
# abort(401) PHP 8 (no dependencies)
<?php
function verifyDiazlunaSignature(string $rawBody, string $headerValue, string $secret): bool {
if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $headerValue, $m)) {
return false;
}
$ts = (int) $m[1];
$sigHex = $m[2];
if (abs(time() - $ts) > 300) {
return false;
}
$expected = hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
return hash_equals($expected, $sigHex);
}
// In your handler:
// $rawBody = file_get_contents('php://input');
// $sigHeader = $_SERVER['HTTP_X_DIAZLUNA_SIGNATURE'] ?? '';
// if (!verifyDiazlunaSignature($rawBody, $sigHeader, getenv('DIAZLUNA_WEBHOOK_SECRET'))) {
// http_response_code(401);
// exit;
// } Important: sign and verify against the raw body (the exact string that arrived), not against a re-serialized JSON. Re-serializing reorders keys and breaks the signature.
v1 event catalog
These are the event_types available today:
| event_type | When it fires |
|---|---|
call.captured | María recorded a call (with or without a transfer) |
call.transferred | María transferred the call to the practice's team line |
call.transfer_failed | María tried to transfer and nobody picked up |
call.appointment_requested | The categorized reason was scheduling |
conversation.message_received | Inbound WhatsApp from a patient/client |
conversation.escalation_created | The bot escalated to human (frustration, cancellation, etc.) |
content.published | The admin published a site change (chat or WhatsApp) |
cap.threshold_reached | Your monthly usage crossed 80% or 100% of your plan cap |
subscription.changed | Your Stripe subscription changed (coming soon) |
customer.created | Conversion completed (admin-side: useful for CRM sync) |
Tools for María (inbound)
While María is on a call, she can invoke functions you define — for example: "Is there availability Tuesday at 2pm?". Diazluna signs and forwards the call to the URL you configure, with a hard 3-second timeout. If your endpoint exceeds the limit or fails, María says a configurable fallback phrase ("Let me check that with the team") and continues the call without error.
Registration
Configure tools in the admin panel under Integrations → Tools. Each tool requires:
- Name (what the LLM uses to invoke):
check_appointment_availability - Description (what the LLM reads when deciding to use the tool)
- Parameters schema (JSON Schema describing the arguments)
- Your endpoint URL
- Timeout (1-5s, default 3s)
- Fallback phrases in Spanish and English
Tool endpoint contract
What you'll receive
POST https://your-domain.com/diazluna-tool
Content-Type: application/json
X-Diazluna-Signature: t=1778302100,v1=a1b2c3d4...
User-Agent: Diazluna-Tool-Proxy/1
{
"args": { "date": "2026-05-15", "time": "14:00" },
"name": "check_appointment_availability",
"call": {
"call_id": "..."
}
} Verify the signature with the same scheme as webhooks (the snippets above apply as-is).
What you should respond
Respond with a JSON object in under 3 seconds. María sees the body of your response as the tool's return value and incorporates it into her next sentence.
HTTP 200
Content-Type: application/json
{
"available_slots": ["10:00", "14:00", "15:30"],
"next_available_day": "2026-05-15"
}
If you need to return plain text, that works too — we wrap it as
{ "result": "<your text>" }.
When something fails
If your endpoint exceeds its timeout_ms, returns 4xx/5xx, or
doesn't respond, María says the fallback phrase and the call continues. We
don't retry (calls are live). Every invocation is logged in
Integrations → Tools → Show recent calls so you can debug.
Support
Technical questions:
[email protected].
Include your event_id or retell_call_id when reporting
an issue — it lets us locate you in the logs in seconds.