Skip to content

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:

  1. Read the X-Diazluna-Signature header.
  2. Split on , to extract t and v1.
  3. Verify t is within the last 5 minutes (replay protection).
  4. Compute HMAC_SHA256(secret, t + "." + raw_body_string) in hex.
  5. Compare with v1 using constant-time comparison (prevents timing attacks). Your language likely has a dedicated function: crypto.timingSafeEqual in Node, hmac.compare_digest in Python, hash_equals in 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_typeWhen it fires
call.capturedMaría recorded a call (with or without a transfer)
call.transferredMaría transferred the call to the practice's team line
call.transfer_failedMaría tried to transfer and nobody picked up
call.appointment_requestedThe categorized reason was scheduling
conversation.message_receivedInbound WhatsApp from a patient/client
conversation.escalation_createdThe bot escalated to human (frustration, cancellation, etc.)
content.publishedThe admin published a site change (chat or WhatsApp)
cap.threshold_reachedYour monthly usage crossed 80% or 100% of your plan cap
subscription.changedYour Stripe subscription changed (coming soon)
customer.createdConversion 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.