Skip to content

Vaspian-LLC/vaspian-webhook-example-cdr

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Vaspian CDR Webhook → Supabase

A simple Python webhook receiver that captures Vaspian CDR (Call Detail Record) events and stores them in a Supabase database.

When a call completes on the Vaspian platform, a CDR webhook is sent to your endpoint with details about the call — direction, caller, destination, duration, and more. This app receives those webhooks and inserts them into a Supabase Postgres table with properly typed columns.

Note: This example focuses specifically on the Call CDR webhook event. Vaspian One also supports other webhook events including Call Ringing, Call Answered, Call Ended, Call Failed, and Call Recorded. The same receiver pattern can be adapted for any of these events — each has its own payload format. See the Vaspian One Webhooks documentation for the full list of available events.

What You'll Need

  • Python 3.10+
  • A Supabase account (free tier works)
  • A Vaspian One account with webhook access
  • Cloudflare Tunnel (cloudflared) for local testing — or any tool that gives your local server a public URL (ngrok, etc.)

CDR Webhook Payload

Vaspian sends CDR webhooks as application/x-www-form-urlencoded POST requests. Every field arrives as a string. Here's what each call produces:

Field Type Example Description
event string Call CDR Always "Call CDR" for CDR webhooks
id string CP02DHB6XNREGRGE Unique call identifier
direction string Outbound Inbound or Outbound
calling_number string 3155652257 Originating phone number
dialed_number string 3155694033 Destination phone number
orig_ext string 188 Originating extension
origination_ts string 1774373567369 Call start (epoch milliseconds)
answer_ts string 1774373573677 Call answered (epoch milliseconds)
end_ts string 1774373607333 Call ended (epoch milliseconds)
total_time string 40 Duration in seconds
is_dcid_call string true Whether Dynamic Caller ID was used
is_recorded string true Whether the call was recorded
groups string Call groups involved
transfer_exts string Extensions involved in transfers

Setup

1. Create the Supabase Table

In your Supabase dashboard, go to the SQL Editor and run:

CREATE TABLE cdr_webhooks (
    id              TEXT PRIMARY KEY,
    direction       TEXT NOT NULL,
    calling_number  TEXT NOT NULL,
    dialed_number   TEXT NOT NULL,
    orig_ext        TEXT,
    origination_ts  TIMESTAMPTZ,
    answer_ts       TIMESTAMPTZ,
    end_ts          TIMESTAMPTZ,
    total_time      INTEGER,
    is_dcid_call    BOOLEAN DEFAULT FALSE,
    is_recorded     BOOLEAN DEFAULT FALSE,
    groups          TEXT DEFAULT '',
    transfer_exts   TEXT DEFAULT '',
    received_at     TIMESTAMPTZ DEFAULT NOW()
);

-- Allow access via the API
ALTER TABLE cdr_webhooks ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Allow all operations" ON cdr_webhooks
    FOR ALL USING (TRUE) WITH CHECK (TRUE);

This is also available in schema.sql.

2. Get Your Supabase Credentials

  1. Open your project in the Supabase dashboard
  2. Go to SettingsData API
  3. Copy your Project URL and Secret key

3. Clone and Configure

git clone https://github.com/vaspian/vaspian-webhook-example-cdr.git
cd vaspian-webhook-example-cdr

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

cp .env.example .env

Edit .env with your Supabase credentials:

SUPABASE_URL=https://your-project-id.supabase.co
SUPABASE_KEY=your-secret-key-here
PORT=5001

4. Start the Receiver

python app.py

You should see:

Starting Vaspian CDR Webhook Receiver on port 5001
 * Running on http://127.0.0.1:5001

5. Expose with a Public URL (for local testing)

Your webhook receiver needs a public URL so Vaspian can reach it. The easiest option is Cloudflare Tunnel:

cloudflared tunnel --url http://localhost:5001

This prints a URL like https://random-words.trycloudflare.com. Your webhook endpoint is:

https://random-words.trycloudflare.com/webhook

6. Configure the Webhook in Vaspian One

  1. Log into Vaspian One and go to Tools → Webhooks (direct link)
  2. Click Add Webhook (or edit an existing one)
  3. Fill in the configuration:
    • Webhook Enabled — Toggle on
    • Webhook Name — Give it a descriptive name
    • Endpoint URL — Your public URL + /webhook (e.g., https://random-words.trycloudflare.com/webhook)
  4. Under Call Flows, check the directions you want to capture (Inbound, Outbound, and/or Internal)
  5. Under Events, check Call CDR
  6. Under Extensions, check all extensions you want to monitor (you must explicitly select each one — unchecked extensions will not trigger webhooks)
  7. Click Submit

7. Test It

Make a call (inbound or outbound) through your Vaspian system. After the call ends, you should see output like:

CDR received: CP02DHB7K413PJS5 Outbound 3155651307 -> 3155694033 (19s)
Stored CDR CP02DHB7K413PJS5 in Supabase

Check your Supabase dashboard — the record will appear in the cdr_webhooks table with all fields properly typed.

How It Works

The app is a single Flask route (/webhook) that:

  1. Receives the form-encoded POST from Vaspian
  2. Validates that the event field is "Call CDR"
  3. Converts string values to proper types (epoch ms → timestamps, "true" → booleans, etc.)
  4. Inserts the typed record into Supabase via the Python client

The key conversion logic is in parse_cdr() in app.py:

def parse_cdr(form: dict) -> dict:
    return {
        "id": form["id"],
        "direction": form["direction"],
        "calling_number": form["calling_number"],
        "dialed_number": form["dialed_number"],
        "orig_ext": form.get("orig_ext", ""),
        "origination_ts": epoch_ms_to_iso(form.get("origination_ts", "")),
        "answer_ts": epoch_ms_to_iso(form.get("answer_ts", "")),
        "end_ts": epoch_ms_to_iso(form.get("end_ts", "")),
        "total_time": int(form.get("total_time", 0)),
        "is_dcid_call": to_bool(form.get("is_dcid_call", "false")),
        "is_recorded": to_bool(form.get("is_recorded", "false")),
        "groups": form.get("groups", ""),
        "transfer_exts": form.get("transfer_exts", ""),
    }

Production Considerations

This example is meant for learning and local testing. For production use, consider:

  • WSGI server — Use Gunicorn or uWSGI instead of Flask's development server
  • Authentication — Add a shared secret or HMAC verification to the webhook endpoint
  • Retry handling — Return appropriate HTTP status codes so Vaspian can retry on failure
  • Duplicate protection — The id primary key already prevents duplicate inserts
  • Monitoring — Add logging, error alerting, and health checks

License

MIT

About

Example: Receive Vaspian CDR webhooks and store them in Supabase

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages