Overview
RealCall can notify your systems via HTTP webhooks when a call or stream finishes processing and a conclusion is reached. This article describes the webhook interface for implementers building a receiver.
When a subscribed event occurs, RealCall sends an HTTP POST to your registered URL with a JSON payload. Each request is signed with an Ed25519 signature so you can verify it came from RealCall and hasn't been tampered with in transit.
Prerequisites
Before setting up a webhook, ensure the following are in place:
Active account: Your organization must have an active account on the Reality Defender platform with access to the RealCall dashboard.
HTTPS endpoint: Your receiver URL must use HTTPS with a valid, publicly trusted TLS certificate. See TLS Requirements below.
Public key storage: When you create a webhook, RealCall displays the Ed25519 public key once. Have a secure location ready to store it.
Setting Up a Webhook
You can create webhooks in the RealCall dashboard.
Step 1: Open the Create Webhook dialog
In the bottom-left corner, click your username, then go to Settings > Webhooks and click Add Webhook. The Create Webhook dialog opens.
Step 2: Fill in the fields
Field | Required | Description |
Name | Yes | A label to identify this webhook in the dashboard (e.g. My Webhook). |
URL | Yes | The HTTPS endpoint that will receive POST requests (e.g. https://example.com/webhook). Must use HTTPS with a valid TLS certificate. |
API Key | No | Optional authentication token. If set, it is sent as the X-API-Key header with every delivery. |
Events | Yes | Select at least one event. Currently only Stream Concluded (stream.concluded) is available and is selected by default. |
Step 3: Click Create
RealCall displays the public key for this webhook (base64-encoded Ed25519). Copy and store it now — it is shown only once and cannot be retrieved later. Use it to verify webhook signatures on your server (see Authentication & Verification).
Note: If you lose the public key, delete the webhook and create a new one to receive a fresh key pair.
The webhook is enabled by default once created. You can edit, disable, or delete it from the same Settings > Webhooks page.
Delivery
Each delivery is a single HTTP POST request.
Property | Value |
Method | POST |
Content-Type | application/json |
Timeout | 10 seconds |
Success | Any 2xx response status |
Failure | Any non-2xx status, timeout, or connection error |
Note: There is currently no automatic retry. A failed delivery is logged but not re-sent. Your endpoint should respond 2xx as soon as it accepts the payload and perform any slow processing asynchronously.
Headers sent with every delivery
Header | Description |
Content-Type | Always application/json |
User-Agent | RealityDefender-Webhook/1.0 |
X-Signature-Ed25519 | Base64-encoded Ed25519 signature of the raw request body |
X-API-Key | Only present if you configured an API key on the webhook |
Payload Format
The body is a JSON envelope with event, timestamp, and data fields:
{
"event": "stream.concluded",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"stream_id": "str_abc123",
"organization_id": "org_xyz789",
"conclusion": "ARTIFICIAL",
"probability": 0.95,
"session_id": "call_001"
}
}event— The event type.timestamp— UTC time the event was emitted (RFC 3339 / ISO 8601).data— Event-specific fields.
Events
Event | Trigger | data fields |
stream.concluded | A call or stream finishes processing and a conclusion is reached | stream_id, organization_id, conclusion, probability, session_id |
stream.concluded is currently the only event type.
data fields for stream.concluded
Field | Type | Description |
stream_id | string | Unique identifier of the analyzed stream. |
organization_id | string | Your organization identifier. |
conclusion | string | The verdict (see values below). Omitted if no result exists. |
probability | number | Confidence score, 0.0–1.0. Omitted if no result exists. |
session_id | string | The originating call or session identifier (e.g. SIP call ID). |
conclusion values
Value | Meaning |
AUTHENTIC | Determined to be a real human voice. |
ARTIFICIAL | Determined to be AI-generated. |
SUSPICIOUS | Shows signs of manipulation but below the ARTIFICIAL threshold. |
INCONCLUSIVE | Analysis ran but could not reach a confident determination. |
If no result record exists for the stream, conclusion and probability are omitted from data.
TLS Requirements
TLS is required for all webhook endpoints. RealCall delivers webhooks over HTTPS and verifies the server's TLS certificate using standard public CA trust. Delivery fails if the certificate cannot be verified.
Requirement | Details |
HTTPS URL | The webhook URL must use https://. Plain http:// may work only for local testing and is not suitable for staging or production. |
Publicly trusted certificate | The certificate must be signed by a publicly trusted CA (e.g. Let's Encrypt, DigiCert). Self-signed certificates and private or internal CAs are not accepted. |
Full certificate chain | The server must present the leaf certificate and all intermediate certificates. A missing intermediate chain causes verification failures. |
Hostname match | The hostname in the webhook URL must match the certificate's Subject Alternative Name (SAN). Use a DNS name (e.g. https://webhooks.example.com/...) rather than a raw IP address unless the certificate explicitly includes that IP in its SAN. |
Common causes of delivery failure
Error or symptom | Likely cause |
x509: certificate signed by unknown authority | Self-signed cert, private CA, or incomplete certificate chain. |
x509: certificate is valid for ... | Webhook URL uses an IP or hostname that does not match the certificate. |
Connection timeout | Port 443 is not reachable from the internet, or a firewall is blocking inbound HTTPS. |
Setting up TLS
A typical setup for a webhook receiver:
Point a DNS name at your server (e.g.
webhooks.example.com→ your server's IP).Obtain a certificate from a public CA (e.g. Let's Encrypt via Certbot).
Configure your web server to serve the full certificate chain (
fullchain.pemin Certbot/nginx terminology).Register the webhook URL using that hostname:
https://webhooks.example.com/your/webhook/pathVerify from outside before going live:
curl -v <https://webhooks.example.com/your/webhook/path>
Confirm the output includes SSL certificate verify ok.
Note: Do not register a webhook URL with a raw IP address (e.g. https://203.0.113.10/...) unless you have a publicly trusted certificate that explicitly includes that IP. Using a DNS hostname with a standard CA-issued certificate is strongly recommended.
Authentication & Verification
Two independent mechanisms are available. You can use either, both, or neither.
Ed25519 signature (recommended)
When you create a webhook, RealCall generates an Ed25519 key pair. The private key is stored on RealCall's side and used to sign every payload; the base64-encoded public key is returned to you once at creation time — store it securely.
Every request includes an X-Signature-Ed25519 header: a base64-encoded Ed25519 signature of the raw request body bytes. To verify:
Read the raw request body (the exact bytes, before any JSON parsing).
Base64-decode the
X-Signature-Ed25519header.Base64-decode your stored public key.
Run Ed25519 verification over the raw body using the public key.
Note: Verify against the raw bytes. If you re-serialize parsed JSON, the bytes (and therefore the signature) may differ and verification will fail.
API key (optional)
The API key is an optional static token you provide when creating the webhook. If set, RealCall forwards it as the X-API-Key header on every delivery. This is useful when your receiver sits behind an API gateway or firewall that requires a static token.
The API key is independent of the signature: the signature proves the payload came from RealCall, while the API key authenticates the request to your server.
Python Example
A complete receiver using Flask that verifies the Ed25519 signature and, optionally, the API key before processing the event. Requires the cryptography library.
pip install flask cryptography
import base64
import os
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from flask import Flask, request, abort
app = Flask(__name__)
# Base64 public key shown once when the webhook was created in RealCall. WEBHOOK_PUBLIC_KEY_B64 = os.environ["REALCALL_WEBHOOK_PUBLIC_KEY"]
# Optional: the API key you configured on the webhook (if any). WEBHOOK_API_KEY = os.environ.get("REALCALL_WEBHOOK_API_KEY")
_public_key = Ed25519PublicKey.from_public_bytes( base64.b64decode(WEBHOOK_PUBLIC_KEY_B64) )
def verify_signature(raw_body: bytes, signature_header: str) -> bool:
if not signature_header:
return False
try:
signature = base64.b64decode(signature_header)
_public_key.verify(signature, raw_body)
return True
except (InvalidSignature, ValueError):
return False
@app.post("/webhook")
def webhook():
# 1. Read the RAW body bytes (do not use request.json here first).
raw_body = request.get_data()
# 2. Verify the Ed25519 signature.
if not verify_signature(raw_body, request.headers.get("X-Signature-Ed25519", "")):
abort(401, "invalid signature")
# 3. (Optional) Check the API key, if you configured one.
if WEBHOOK_API_KEY and request.headers.get("X-API-Key") != WEBHOOK_API_KEY:
abort(401, "invalid api key")
# 4. Signature valid — now it is safe to parse and handle the payload.
payload = request.get_json()
event = payload.get("event")
data = payload.get("data", {})
if event == "stream.concluded":
print(
f"stream {data.get('stream_id')} concluded: "
f"{data.get('conclusion')} (probability={data.get('probability')})"
)
# TODO: enqueue async processing here; keep this handler fast.
# 5. Respond 2xx promptly so the delivery is marked successful.
return "", 200
if __name__ == "__main__":
app.run(port=9999)
To run:
export REALCALL_WEBHOOK_PUBLIC_KEY="<base64 public key from webhook creation>"
# export REALCALL_WEBHOOK_API_KEY="<your api key>" # only if configured
python app.py
Verifying without Flask
The verification logic only requires the raw body, the signature header, and the public key:
import base64
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
def is_valid(raw_body: bytes, signature_b64: str, public_key_b64: str) -> bool:
public_key = Ed25519PublicKey.from_public_bytes(base64.b64decode(public_key_b64))
try:
public_key.verify(base64.b64decode(signature_b64), raw_body)
return True
except (InvalidSignature, ValueError):
return False
Receiver Checklist
Expose an HTTPS endpoint with a publicly trusted certificate and full chain (see TLS Requirements).
Use a DNS hostname in the webhook URL that matches the certificate.
Accept POST requests with a JSON body.
Read and verify the raw body against
X-Signature-Ed25519using your stored public key.(Optional) Validate the
X-API-Keyheader if you configured an API key.Respond with a 2xx status within 10 seconds.
Perform slow processing asynchronously — there are no automatic retries.
