D Line API

REST and webhooks for calls, texts, voicemail and your workspace.

Quickstart

1. Create an API key

A workspace admin creates keys in Settings > Developers in the D Line portal. Pick only the scopes you need (for example workspace:read and calls:read). The secret is shown once; store it in your secrets manager. Live keys start with dl_live_, test keys with dl_test_. Test keys run the full API but never reach carriers or phones.

2. Make your first request

GET /v1/me returns the workspace, the key's scopes and what the workspace can use. Every SDK calls it first.

curl https://api.d-line.app/v1/me \
  -H "Authorization: Bearer $DLINE_API_KEY"
{
  "workspace": { "id": "ws_01J9ZJ0Q8M7Y4K2T5V6W8X9Y0Z", "name": "Turn Around Trucking", "plan": "advanced", "timezone": "America/Chicago" },
  "credential": { "type": "api_key", "id": "key_01J9ZJ3H4S5T6V7V8W9X0Y1Z2A", "name": "CRM sync", "mode": "live",
                  "scopes": ["workspace:read", "messages:read", "calls:read", "webhooks:write"] },
  "capabilities": { "webhooks": true, "sms": false, "dialer": false, "ai_caller": false }
}

Every response carries DLine-Request-Id and RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset. On 429 wait Retry-After seconds. Errors use one envelope; see Errors.

3. Receive a webhook

Create an endpoint. The response includes its signing secret (whsec_…) once.

curl https://api.d-line.app/v1/webhook-endpoints \
  -H "Authorization: Bearer $DLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/dline", "events": ["call.missed", "message.received"] }'

Deliveries follow Standard Webhooks. Each request has webhook-id (the event id, the same on every retry), webhook-timestamp and webhook-signature: v1, + base64 HMAC-SHA256 of webhook-id.webhook-timestamp.body, keyed with the base64-decoded part of the secret after whsec_. An older secret without the whsec_ prefix is used as its raw UTF-8 bytes (the snippets below handle both). During a secret rotation the header holds two signatures; accept either. Reject timestamps more than five minutes off. Answer any 2xx within 10 seconds; failures are retried for about three days. The official standardwebhooks libraries also work.

Node.js

import crypto from 'node:crypto';

export function verifyDLineWebhook(rawBody, headers, secret) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatures = headers['webhook-signature'];
  if (!id || !timestamp || !signatures) throw new Error('Missing webhook headers');
  const ts = Number(timestamp);
  if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) throw new Error('Webhook timestamp is too old');

  // whsec_ secrets are base64; older secrets without the prefix are used as raw UTF-8 bytes.
  const key = secret.startsWith('whsec_') ? Buffer.from(secret.slice(6), 'base64') : Buffer.from(secret, 'utf8');
  const expected = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest();
  const valid = signatures.split(' ').some((entry) => {
    const [version, signature] = entry.split(',');
    if (version !== 'v1' || !signature) return false;
    const given = Buffer.from(signature, 'base64');
    return given.length === expected.length && crypto.timingSafeEqual(given, expected);
  });
  if (!valid) throw new Error('Invalid webhook signature');
  return JSON.parse(rawBody);
}
import express from 'express';
import { verifyDLineWebhook } from './verify-dline-webhook.mjs';

const app = express();

// Verify against the raw bytes: parsing and re-serialising JSON breaks the signature.
app.post('/hooks/dline', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try {
    event = verifyDLineWebhook(req.body.toString('utf8'), req.headers, process.env.DLINE_WEBHOOK_SECRET);
  } catch {
    return res.sendStatus(400);
  }
  // Deduplicate on event.id (same as the webhook-id header), then do the work async.
  console.log(event.type, event.data.object.id);
  res.sendStatus(204);
});

app.listen(3000);

Python

import base64
import hashlib
import hmac
import json
import time


def verify_dline_webhook(raw_body: bytes, headers, secret: str) -> dict:
    msg_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    signatures = headers["webhook-signature"]
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("Webhook timestamp is too old")

    # whsec_ secrets are base64; older secrets without the prefix are used as raw UTF-8 bytes.
    key = base64.b64decode(secret[6:]) if secret.startswith("whsec_") else secret.encode()
    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    for entry in signatures.split(" "):
        version, _, signature = entry.partition(",")
        if version == "v1" and hmac.compare_digest(signature, expected):
            return json.loads(raw_body)
    raise ValueError("Invalid webhook signature")
import os
from flask import Flask, request

from verify_dline_webhook import verify_dline_webhook

app = Flask(__name__)


@app.post("/hooks/dline")
def dline_webhook():
    try:
        event = verify_dline_webhook(request.get_data(), request.headers, os.environ["DLINE_WEBHOOK_SECRET"])
    except (KeyError, ValueError):
        return "", 400
    # Deduplicate on event["id"] (same as the webhook-id header), then do the work async.
    print(event["type"], event["data"]["object"]["id"])
    return "", 204

4. Test it

POST /v1/webhook-endpoints/{id}/test queues a signed webhook.test event to your endpoint right away; the returned delivery shows the status code and latency once it is attempted. Browse every endpoint and event in the API reference.