Skip to content

SIP VAULT CDR Integration Guide

This guide covers integrating SIP VAULT with CDR Viewer applications so that operators can open call troubleshooting data directly from CDR records with a single click.

Table of Contents


Overview

SIP VAULT provides a "deep link" mechanism that allows CDR Viewer applications to generate authenticated URLs pointing directly to a specific call's troubleshooting data. When an operator clicks the link, the SIP VAULT dashboard opens with the full SIP capture, RTCP quality analysis, and OpenSIPS logs for that call.

The integration is lightweight: a single function that takes a Call-ID, computes an HMAC-SHA256 signature with a timestamp, and returns a URL. No API calls, no callbacks, no session state. The call date does not need to be passed — the API resolves it from S3 automatically.

How It Works

1. Operator views CDR list in CDR Viewer (e.g., OpenSIPS Control Panel)
2. Each CDR row has a "SIP VAULT" button
3. On click, the CDR Viewer server generates an HMAC-signed URL:
   a. Hash the Call-ID:  call_hash = SHA-256(call_id)[:16]
   b. Get current hour:  ts_hour = unix_timestamp / 3600  (integer division)
   c. Build payload:     "{call_id}:{ts_hour}"
   d. Sign the payload:  sig = HMAC-SHA256(secret, payload).hex()[:32]
   e. Build the URL:     https://sipvault.example.com/call/{call_hash}
                           ?cid={customer_id}&callid={call_id}&ts={ts_hour}&sig={sig}
4. User's browser opens the URL
5. SIP VAULT dashboard validates the signature and loads the call data

The HMAC secret used for signing must match the SIPVAULT_HMAC_SECRET configured in /etc/sipvault/api.env on the SIP VAULT server. The secret never leaves the server side.

Tokens are valid for ±1 hour from the hour in which they were generated.


PHP Integration (OpenSIPS Control Panel)

This is the primary integration for OpenSIPS Control Panel (OpenSIPS CP).

Installation

Copy the file deploy/cdr-integration/generate_link.php to your OpenSIPS CP installation.

Configuration

Edit the constants at the top of the file to match your deployment:

define('SIPVAULT_BASE_URL', 'https://sipvault.sippulse.com.br');
define('SIPVAULT_HMAC_SECRET', 'your_hmac_secret_here');  // Must match api.env
define('SIPVAULT_CUSTOMER_ID', 'sippulse');

Full Source Code

<?php
/**
 * SIP VAULT link generator for OpenSIPS CP / CDR Viewer integration.
 *
 * Usage in CDR list template:
 *   <?php echo sipvault_link($row['call_id']); ?>
 *
 * Or as a button:
 *   <a href="<?php echo sipvault_link($cdr['callid']); ?>"
 *      target="_blank" class="btn btn-sm btn-info">
 *      SIP VAULT
 *   </a>
 */

// Configuration — must match deploy/api.env SIPVAULT_HMAC_SECRET
define('SIPVAULT_BASE_URL', 'https://sipvault.sippulse.com.br');
define('SIPVAULT_HMAC_SECRET', 'YOUR_HMAC_SECRET_HERE');
define('SIPVAULT_CUSTOMER_ID', 'sippulse');

/**
 * Generate an HMAC-signed SIP VAULT dashboard URL for a call.
 *
 * @param string $call_id   The SIP Call-ID header value
 * @param string $call_time Unused — kept for API compat. The API resolves the date from S3.
 * @param string $customer  Customer identifier (default: SIPVAULT_CUSTOMER_ID)
 * @return string Full URL with authentication token (valid ~1 hour)
 */
function sipvault_link($call_id, $call_time = null, $customer = SIPVAULT_CUSTOMER_ID) {
    $call_hash = substr(hash('sha256', $call_id), 0, 16);
    $ts_hour = (int)(time() / 3600);
    $payload = "{$call_id}:{$ts_hour}";
    $sig = substr(hash_hmac('sha256', $payload, SIPVAULT_HMAC_SECRET), 0, 32);
    return SIPVAULT_BASE_URL . '/call/' . $call_hash
        . '?cid=' . urlencode($customer)
        . '&callid=' . urlencode($call_id)
        . '&ts=' . $ts_hour
        . '&sig=' . $sig;
}

/**
 * Generate an HTML button/link for the CDR table.
 */
function sipvault_button($call_id, $call_time = null, $customer = SIPVAULT_CUSTOMER_ID) {
    $url = sipvault_link($call_id, $call_time, $customer);
    return '<a href="' . htmlspecialchars($url) . '" target="_blank" '
         . 'class="btn btn-sm btn-info" title="Open in SIP VAULT">'
         . '<i class="fa fa-search"></i> SIP VAULT</a>';
}

// CLI usage
if (php_sapi_name() === 'cli' && isset($argv[1])) {
    $customer = isset($argv[2]) ? $argv[2] : SIPVAULT_CUSTOMER_ID;
    echo sipvault_link($argv[1], null, $customer) . "\n";
}

Usage in Templates

Simple link:

<?php echo sipvault_link($row['call_id']); ?>

Button with icon:

<?php echo sipvault_button($row['call_id']); ?>

Inline HTML:

<a href="<?php echo sipvault_link($cdr['callid']); ?>"
   target="_blank" class="btn btn-sm btn-info">
   <i class="fa fa-search"></i> SIP VAULT
</a>

CLI Testing

php generate_link.php "a7a186b1ff8c4cfeaa90a9144ba9cb24"

Python Integration

For CDR Viewer applications built with Python (Django, Flask, FastAPI, etc.).

Installation

Copy deploy/cdr-integration/generate_link.py to your project.

Configuration

Edit the constants at the top of the file:

SIPVAULT_BASE_URL = "https://sipvault.sippulse.com.br"
SIPVAULT_HMAC_SECRET = "your_hmac_secret_here"  # Must match api.env

Full Source Code

"""
SIP VAULT link generator for CDR Viewer integration.

Usage:
    from generate_link import sipvault_link

    url = sipvault_link(
        call_id="a7a186b1ff8c4cfeaa90a9144ba9cb24",
        customer_id="sippulse",
    )
"""

import hashlib
import hmac
import time
from urllib.parse import quote

# Configuration — match deploy/api.env SIPVAULT_HMAC_SECRET
SIPVAULT_BASE_URL = "https://sipvault.sippulse.com.br"
SIPVAULT_HMAC_SECRET = "YOUR_HMAC_SECRET_HERE"


def sipvault_link(
    call_id: str,
    call_date: str | None = None,  # unused, kept for API compat
    customer_id: str = "sippulse",
    secret: str = SIPVAULT_HMAC_SECRET,
    base_url: str = SIPVAULT_BASE_URL,
) -> str:
    """Generate an HMAC-signed SIP VAULT dashboard URL for a call.

    Args:
        call_id: The SIP Call-ID header value
        call_date: Ignored (the API resolves the date from S3). Kept for compat.
        customer_id: Customer identifier
        secret: HMAC secret (must match API config)
        base_url: SIP VAULT dashboard base URL

    Returns:
        Full URL with authentication token (valid ~1 hour)
    """
    call_hash = hashlib.sha256(call_id.encode()).hexdigest()[:16]
    ts_hour = int(time.time()) // 3600
    payload = f"{call_id}:{ts_hour}".encode()
    sig = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()[:32]
    return (
        f"{base_url}/call/{call_hash}"
        f"?cid={quote(customer_id)}&callid={quote(call_id)}&ts={ts_hour}&sig={sig}"
    )


if __name__ == "__main__":
    import sys
    if len(sys.argv) < 2:
        print("Usage: python generate_link.py <call_id> [customer_id]")
        sys.exit(1)
    cid = sys.argv[2] if len(sys.argv) > 2 else "sippulse"
    print(sipvault_link(call_id=sys.argv[1], customer_id=cid))

Usage in Django

from generate_link import sipvault_link

def cdr_detail(request, cdr_id):
    cdr = CDR.objects.get(id=cdr_id)
    vault_url = sipvault_link(
        call_id=cdr.call_id,
        customer_id="acme",
    )
    return render(request, "cdr_detail.html", {"cdr": cdr, "vault_url": vault_url})

Usage in Flask

from generate_link import sipvault_link

@app.route("/cdr/<int:cdr_id>")
def cdr_detail(cdr_id):
    cdr = get_cdr(cdr_id)
    vault_url = sipvault_link(call_id=cdr["call_id"])
    return render_template("cdr_detail.html", cdr=cdr, vault_url=vault_url)

CLI Testing

python generate_link.py "a7a186b1ff8c4cfeaa90a9144ba9cb24"

Node.js Integration

For CDR Viewer applications built with Node.js (Express, Next.js, etc.).

Installation

Copy deploy/cdr-integration/generate_link.js to your project.

Configuration

Set environment variables or edit the defaults in the file:

export SIPVAULT_BASE_URL="https://sipvault.sippulse.com.br"
export SIPVAULT_HMAC_SECRET="your_hmac_secret_here"
export SIPVAULT_CUSTOMER_ID="sippulse"

Full Source Code

/**
 * SIP VAULT link generator for Node.js / Next.js CDR integration.
 *
 * Usage:
 *   import { sipvaultLink } from './generate_link'
 *   const url = sipvaultLink('call-id-here')
 */

const crypto = require('crypto')

// Configuration — must match deploy/api.env SIPVAULT_HMAC_SECRET
const SIPVAULT_BASE_URL = process.env.SIPVAULT_BASE_URL || 'https://sipvault.sippulse.com.br'
const SIPVAULT_HMAC_SECRET = process.env.SIPVAULT_HMAC_SECRET || 'YOUR_HMAC_SECRET_HERE'
const SIPVAULT_CUSTOMER_ID = process.env.SIPVAULT_CUSTOMER_ID || 'sippulse'

/**
 * Generate an HMAC-signed SIP VAULT dashboard URL for a call.
 *
 * @param {string} callId - The SIP Call-ID header value
 * @param {string} [_callDate] - Unused. Kept for API compat. The API resolves date from S3.
 * @param {string} [customerId] - Customer identifier
 * @returns {string} Full URL with authentication token (valid ~1 hour)
 */
function sipvaultLink(callId, _callDate, customerId = SIPVAULT_CUSTOMER_ID) {
  const callHash = crypto.createHash('sha256').update(callId).digest('hex').substring(0, 16)
  const tsHour = Math.floor(Date.now() / 1000 / 3600)
  const payload = `${callId}:${tsHour}`
  const sig = crypto.createHmac('sha256', SIPVAULT_HMAC_SECRET).update(payload).digest('hex').substring(0, 32)
  return (
    `${SIPVAULT_BASE_URL}/call/${callHash}`
    + `?cid=${encodeURIComponent(customerId)}`
    + `&callid=${encodeURIComponent(callId)}`
    + `&ts=${tsHour}`
    + `&sig=${sig}`
  )
}

module.exports = { sipvaultLink }

// CLI usage
if (require.main === module) {
  const callId = process.argv[2]
  if (!callId) {
    console.error('Usage: node generate_link.js <call_id> [customer_id]')
    process.exit(1)
  }
  const customerId = process.argv[3] || SIPVAULT_CUSTOMER_ID
  console.log(sipvaultLink(callId, null, customerId))
}

Usage in Express

const { sipvaultLink } = require('./generate_link')

app.get('/cdr/:id', (req, res) => {
  const cdr = getCDR(req.params.id)
  const vaultUrl = sipvaultLink(cdr.callId)
  res.render('cdr_detail', { cdr, vaultUrl })
})

Usage in Next.js API Route

import { sipvaultLink } from '../../lib/generate_link'

export default function handler(req, res) {
  const { callId } = req.query
  const url = sipvaultLink(callId)
  res.json({ url })
}

CLI Testing

node generate_link.js "a7a186b1ff8c4cfeaa90a9144ba9cb24"

OpenSIPS CP CDR Viewer Template Modification

To add the SIP VAULT button to the OpenSIPS Control Panel CDR Viewer, modify the CDR list template.

Copy deploy/cdr-integration/generate_link.php to your OpenSIPS CP installation directory, typically:

cp deploy/cdr-integration/generate_link.php /var/www/opensips-cp/web/tools/cdrviewer/

Step 2: Edit the CDR Viewer Template

Edit the CDR Viewer main template. The typical file path is:

/var/www/opensips-cp/web/tools/cdrviewer/cdrviewer.main.php

Add the include at the top of the file:

<?php require_once __DIR__ . '/generate_link.php'; ?>

Step 3: Add the SIP VAULT Column

Find the CDR table header row and add a new column:

<th>SIP VAULT</th>

Find the CDR table body row and add the button cell:

<td><?php echo sipvault_button($cdr['callid']); ?></td>

Step 4: Verify

Reload the CDR Viewer page. Each CDR row should now have a "SIP VAULT" button that opens the call data in a new tab.


Custom CDR Viewer Integration

For CDR Viewer applications built with any web framework, implement the following algorithm.

Algorithm

function generate_sipvault_url(call_id, customer_id, secret, base_url):
    # 1. Hash the Call-ID for the URL path (first 16 hex chars of SHA-256)
    call_hash = SHA256(call_id).hex()[:16]

    # 2. Get the current hour bucket (Unix timestamp / 3600, integer division)
    ts_hour = unix_timestamp() / 3600

    # 3. Build the HMAC payload
    payload = "{call_id}:{ts_hour}"

    # 4. Compute HMAC-SHA256 signature, truncate to 32 hex chars
    sig = HMAC_SHA256(key=secret, message=payload).hex()[:32]

    # 5. Build the URL
    url = "{base_url}/call/{call_hash}?cid={customer_id}&callid={call_id}&ts={ts_hour}&sig={sig}"

    return url

Requirements

  • call_id: The raw SIP Call-ID header value (string)
  • customer_id: Your SIP VAULT customer identifier (string)
  • secret: The HMAC secret matching SIPVAULT_HMAC_SECRET in /etc/sipvault/api.env (string)
  • base_url: Your SIP VAULT dashboard URL, e.g., https://sipvault.sippulse.com.br (string)

Important Notes

  • No call_date is needed — the API scans S3 to find the call automatically
  • The ts_hour is unix_timestamp / 3600 (integer division), not the raw timestamp
  • Tokens are valid for ±1 hour relative to the hour bucket they were generated in
  • The SHA-256 hash uses only the first 16 hex characters (8 bytes)
  • The HMAC signature is truncated to the first 32 hex characters

URL Parameters Explained

Parameter Source Purpose
cid Your SIP VAULT account Identifies the customer for bucket lookup and tenant isolation
callid SIP Call-ID from CDR Used to locate the call data in S3; also covered by the HMAC signature
ts unix_timestamp / 3600 at generation time Hour bucket; determines token validity window (±1 hour)
sig First 32 hex chars of HMAC-SHA256("{callid}:{ts}", secret) Proves the link was generated by someone with the HMAC secret

Token Lifetime

Tokens are valid within a ±1 hour window of the hour bucket in which they were generated:

  • A token generated at 14:37 has ts_hour = 14 (UTC hours since epoch)
  • It remains valid until ts_hour = 16 (i.e., until 16:00 UTC that day)
  • To extend the session, the operator must return to the CDR Viewer and click the link again

Testing the Integration

Use the CLI to generate a link:

PHP:

php deploy/cdr-integration/generate_link.php "test-call-id-123"

Python:

python deploy/cdr-integration/generate_link.py "test-call-id-123"

Node.js:

node deploy/cdr-integration/generate_link.js "test-call-id-123"

All three should produce URLs in this format:

https://sipvault.sippulse.com.br/call/XXXXXXXXXXXXXXXX?cid=sippulse&callid=test-call-id-123&ts=NNNNNN&sig=HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH

Open the generated URL in a browser. You should see:

  • If the call exists in S3: The dashboard loads with call data
  • If the call does NOT exist in S3: The dashboard shows "Call not found" (expected for a test Call-ID)
  • If the signature is invalid: HTTP 401 error

Step 3: Verify Expiry

Tokens expire after ±1 hour. To test expiry quickly, generate a link and wait until the next full hour boundary plus one hour. The URL should then return HTTP 401 with "invalid signature".

Step 4: Cross-Language Verification

Generate URLs with the same Call-ID using all three language implementations. The call_hash in the URL path should be identical across all three. The ts and sig values will differ if generated at different times, but all should be accepted by the API when fresh.


Troubleshooting

401 Unauthorized: "missing signature"

The request did not include a sig query parameter. Ensure the link generator is adding &sig=... to the URL.

401 Unauthorized: "invalid signature"

The HMAC signature does not match. Check:

  1. The SIPVAULT_HMAC_SECRET in your link generator matches the value in /etc/sipvault/api.env exactly
  2. There are no trailing whitespace characters or newlines in either the generator constant or the environment variable
  3. The callid value is URL-decoded correctly when passed to HMAC verification

401 Unauthorized: "ts expired" or token rejected

The ts hour bucket is outside the ±1 hour window. This happens when:

  • The operator waited more than 2 hours before clicking the link
  • The server clocks are out of sync between the CDR Viewer server and the SIP VAULT server

Fix: Ensure NTP is running on both servers (chronyc tracking).

"Call not found" on the Dashboard (404)

The token is valid, but no data exists in S3 for this Call-ID. Check:

  1. The callid is the complete SIP Call-ID header value (not a CDR record ID or other identifier)
  2. The agent was running and connected to the server at the time of the call
  3. The call completed (BYE/CANCEL) so that the server session transitioned to COMPLETE and wrote data to S3
  4. The call is not older than 7 days (the API scans the last 7 days only)

403 Forbidden: "call_hash does not match token"

The call_hash in the URL path does not match the SHA-256 hash of the callid parameter. This means the link was constructed incorrectly. Verify:

echo -n "the-call-id" | sha256sum | cut -c1-16

The result should match the hash in the URL path.

Browser Shows Raw JSON or Error

Ensure the dashboard SPA is correctly built and deployed at /opt/sipvault/dashboard/. The nginx configuration must serve index.html as the fallback for all routes:

location / {
    try_files $uri $uri/ /index.html;
}