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 "", 2044. 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.