For developers

Connect your CRM

Every account already includes Prospector CRM — this is for connecting something else, or driving Contact Dynamite from your own systems. Two directions, use either or both. Push: call the REST API to send a video, sync a contact, or fire an automation. Pull: point a webhook at your own service and receive every send, watch, button tap and video reply as it happens.

Built-in connectors: Prospector CRM (included, sends and logs), Follow Up Boss and Wise Agent(log activity; delivery runs through us). BoldTrail/kvCORE and KW Command need a vendor agreement we don't hold yet — use the webhook below meanwhile.

Authentication

Create a key in Settings → CRM. Keys are shown once and scoped to a single account. Pass it as a bearer token:

Authorization: Bearer cd_live_xxxxxxxxxxxxxxxxxxxx

X-API-Key: <key> works too, if that's easier for your platform.

Send a video

The endpoint that makes “send a Contact Dynamite video” a step in any CRM workflow. Delivers the email or text and starts tracking immediately.

POST/api/v1/sends

curl -X POST https://contact-dynamite-d73mzui8k-thunderbird-agency.vercel.app/api/v1/sends \
  -H "Authorization: Bearer $CD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "videoId": "abc123",
    "channel": "email",
    "contact": {
      "name": "Dana Reyes",
      "email": "dana@example.com",
      "phone": "+15555550147"
    },
    "message": "Quick update on the Maple St listing.",
    "pathways": [
      { "key": "book_call" },
      { "key": "see_listing", "url": "https://…/123-maple" }
    ]
  }'

Returns 201 with the watch URL when delivery succeeded, or 202 when the send was recorded but no sender is configured — use the returned watchUrl to deliver it yourself.

Videos

List the account's library to pick one to send.

GET/api/v1/videos?status=ready&limit=50

{
  "videos": [
    {
      "id": "abc123",
      "title": "Maple St — just listed",
      "status": "ready",
      "durationSeconds": 42,
      "shareUrl": "https://contact-dynamite-d73mzui8k-thunderbird-agency.vercel.app/v/abc123",
      "createdAt": "2026-08-20T17:04:11.000Z"
    }
  ]
}

Contacts

Push contacts in from your CRM. Idempotent on email, then phone — safe to re-sync on every change.

POST/api/v1/contacts

GET/api/v1/contacts?limit=100

curl -X POST https://contact-dynamite-d73mzui8k-thunderbird-agency.vercel.app/api/v1/contacts \
  -H "Authorization: Bearer $CD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Dana Reyes",
    "email": "dana@example.com",
    "crmContactId": "your-crm-id-here"
  }'
{
  "contact": {
    "id": "…",
    "name": "Dana Reyes",
    "email": "dana@example.com",
    "phone": null,
    "label": null,
    "crmContactId": "your-crm-id-here",
    "createdAt": "2026-08-21T13:58:02.000Z"
  }
}

Pass crmContactId to keep your own identifier on the record, so engagement webhooks come back with an id you already know. A new contact returns 201, a matched one 200.

Engagement

For CRMs that would rather poll than receive webhooks. Filter with since to pull incrementally.

GET/api/v1/engagement?since=2026-08-01T00:00:00Z

{
  "sends": [
    {
      "sendId": "…",
      "watchUrl": "https://contact-dynamite-d73mzui8k-thunderbird-agency.vercel.app/v/tok_…",
      "status": "watched",
      "contact": { "email": "dana@example.com", "crmContactId": "…" },
      "maxWatchPercent": 100,
      "openedAt": "2026-08-21T14:02:00.000Z",
      "firstWatchedAt": "2026-08-21T14:02:19.000Z"
    }
  ]
}

Automation triggers

Fire a Dynamite Auto rule from your CRM — a new lead, a birthday, a price drop. Requires the Dynamite Auto plan.

POST/api/v1/triggers

curl -X POST https://contact-dynamite-d73mzui8k-thunderbird-agency.vercel.app/api/v1/triggers \
  -H "Authorization: Bearer $CD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger": "new_lead",
    "contact": { "name": "Dana Reyes", "email": "dana@example.com" },
    "merge": { "area": "Phoenix", "detail": "3bd ranch under $600k" }
  }'

Triggers: new_lead, birthday, price_drop, market_update, campaign.

Engagement webhooks

Set a webhook URL in Settings → CRM and every event is POSTed to it as JSON. This is how a CRM we don't integrate with natively — BoldTrail, KW Command, anything in-house — still gets a full timeline.

EventFires when
video.sentA video is delivered to a contact
video.openedThe email is opened, or the watch page is loaded
video.playedThe contact starts watching
video.progressA 25 / 50 / 75% milestone is passed
video.completedThe contact watches to the end
pathway.tappedA reply button is tapped
reply.receivedThe contact records a video reply
contact.syncedA contact is created or updated
POST https://your-crm.example.com/hooks/dynamite
X-Dynamite-Event: pathway.tapped
X-Dynamite-Timestamp: 1787000000
X-Dynamite-Signature: sha256=…

{
  "event": "pathway.tapped",
  "occurredAt": "2026-08-21T14:03:44.000Z",
  "contact": {
    "name": "Dana Reyes",
    "email": "dana@example.com",
    "crmContactId": "your-crm-id-here"
  },
  "video": {
    "id": "abc123",
    "title": "Maple St — just listed",
    "watchUrl": "https://contact-dynamite-d73mzui8k-thunderbird-agency.vercel.app/v/tok_…"
  },
  "data": { "pathwayKey": "book_call", "pathwayLabel": "Book a call" }
}

Verifying a webhook

Each delivery is signed with the shared secret you set. Recompute the HMAC over <timestamp>.<raw body> and compare in constant time. Reject anything older than five minutes.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(req, rawBody, secret) {
  const timestamp = req.headers["x-dynamite-timestamp"];
  const signature = req.headers["x-dynamite-signature"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected =
    "sha256=" + createHmac("sha256", secret)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && timingSafeEqual(a, b);
}

Respond 2xx to acknowledge. Anything else is retried with backoff — roughly 1 minute, 5, 15, 1 hour, then 6 — so a brief outage on your side delays the sync rather than losing it.

Errors

400Malformed request — the body explains what's missing
401Missing, revoked, or unrecognised API key
402The account's subscription or Auto plan is inactive
404No such video or contact on this account
409The video is still processing — retry shortly