Beta

Safina API reference

Read assistants, calls and contacts, create and update contacts, and get a webhook after each call. Automation platforms such as Zapier, Make and n8n use the same API.


The OpenAPI document is at https://api.backend.safina.ai/openapi.json.

Authentication

Create an API key in the Safina app: open Profile > API keys. Only the owner or an admin of a team can create a key, and the team needs the Pro plan or higher. Choose read only or read and write, and when the key expires: after 30 days, 90 days, 1 year, or never. The app shows the key only once. Send it in the header Authorization: Bearer safina_api_….

A key works only for this API and only for its team. It does not work for the Safina MCP server: AI assistants connect as described in Connect Safina to your AI assistant. Delete a key in the app when you no longer need it.

Webhook signature

Signing is optional. To sign the events of a webhook, set create_signing_secret: true in POST /webhooks. The answer then holds signing_secret (whsec_…) only once: store it in your receiver. Without create_signing_secret, the events are not signed. For a webhook without a secret, the owner can create one later in the Safina app: Safina AI > CRM Synchronization > Create secret. Each signed event has the header X-Safina-Signature: t=<unix seconds>,v1=<signature>. The signature is the hex HMAC-SHA256 of <t>.<raw body>, with the signing secret as the key.

  • Refuse the event when t is more than 5 minutes (300 seconds) before or after your clock.
  • Calculate the HMAC over the raw request body exactly as received, before you parse the JSON.
  • Compare it with each v1 value in constant time. One match is sufficient: after a new secret, the old secret stays valid for 24 hours, and the header has one v1 value for each valid secret.
  • Each delivery attempt has a new timestamp and a new signature. A retry sends the same event_id, so drop an event_id that you already processed.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as received (a Buffer or a string)
const verifySafinaSignature = (header, rawBody, secret) => {
  const parts = header.split(",").map((part) => part.trim().split("="));
  const timestamp = Number(parts.find(([key]) => key === "t")?.[1]);
  if (!Number.isInteger(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = Buffer.from(
    createHmac("sha256", secret)
      .update(`${timestamp}.`)
      .update(rawBody)
      .digest("hex")
  );
  return parts.some(([key, value]) => {
    const candidate = Buffer.from(value ?? "");
    return (
      key === "v1" &&
      candidate.length === expected.length &&
      timingSafeEqual(candidate, expected)
    );
  });
};

Python

import hashlib
import hmac
import time


# raw_body: the request body exactly as received, as bytes
def verify_safina_signature(header: str, raw_body: bytes, secret: str) -> bool:
    parts = [part.strip().split("=", 1) for part in header.split(",")]
    values = [(part[0], part[1]) for part in parts if len(part) == 2]
    timestamp = next((value for key, value in values if key == "t"), "")
    if not (timestamp.isascii() and timestamp.isdigit()):
        return False
    if abs(time.time() - int(timestamp)) > 300:
        return False
    expected = hmac.new(
        secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return any(
        key == "v1" and hmac.compare_digest(value.encode(), expected.encode())
        for key, value in values
    )

You can also set auth_type on a webhook, so that Safina sends a bearer token or an API key with each event.

Safina Support Bot
Safina Support Bot

Hi! 👋 I'm the Safina support assistant. Ask me anything about setup, call forwarding, plans or your account. Note: your messages are processed in line with our privacy policy.

Your messages are processed per our privacy policy