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
tis 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
v1value in constant time. One match is sufficient: after a new secret, the old secret stays valid for 24 hours, and the header has onev1value for each valid secret. -
Each delivery attempt has a new timestamp and a new signature. A
retry sends the same
event_id, so drop anevent_idthat 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.