API & Webhooks
REST v1 · Base URL https://tellsign.app/api/v1
The Tellsign API lets you run opportunity scans and pull ranked leads from your own tools, and webhooks push events to you in real time. The API and webhooks are available on every paid plan.
Authentication
Every request is authenticated with your workspace API key as a Bearer token. Generate or rotate the key in Settings → API access. Keep it secret — it acts on behalf of your whole workspace.
Authorization: Bearer lof_xxxxxxxxxxxxxxxxxxxxxxxxAll requests must be over HTTPS. Calls are rate-limited per workspace; if you exceed the limit you'll get 429 with a Retry-After header.
Run a scan
POST /api/v1/campaigns creates a campaign and runs it synchronously — the response returns once the scan finishes. This consumes leads from your monthly plan allowance.
Body fields: niche and city (required), serviceType (required), and optional name, radiusKm (default 25), centerAddress / centerLat / centerLng (target a radius around a point), maxLeads (default 20, max 200), and a filters object (e.g. requireWebsite, maxMobileScore, excludeChains, requireNoBooking).
curl -X POST https://tellsign.app/api/v1/campaigns \
-H "Authorization: Bearer $TELLSIGN_KEY" \
-H "Content-Type: application/json" \
-d '{
"niche": "dentists",
"city": "Austin",
"serviceType": "website_redesign",
"maxLeads": 25,
"filters": { "requireWebsite": true, "maxMobileScore": 60 }
}'200 OK
{ "id": "cmq…", "name": "dentists — Austin", "discovered": 60, "kept": 25 }If your trial has ended or your plan quota is exhausted you'll get 402 with a code of TRIAL_EXPIRED or QUOTA_EXCEEDED.
List campaigns
GET /api/v1/campaigns returns your campaigns with lead counts, newest first.
curl https://tellsign.app/api/v1/campaigns \
-H "Authorization: Bearer $TELLSIGN_KEY"
{ "campaigns": [
{ "id": "cmq…", "name": "dentists — Austin", "niche": "dentists",
"city": "Austin", "serviceType": "website_redesign",
"status": "complete", "leads": 25, "createdAt": "…" }
] }List leads
GET /api/v1/campaigns/{id}/leads returns the scored leads for a campaign — business details, the opportunity score, an audit summary, and a public report URL when one exists.
curl https://tellsign.app/api/v1/campaigns/cmq…/leads \
-H "Authorization: Bearer $TELLSIGN_KEY"
{ "leads": [
{ "id": "cmq…", "status": "new", "locked": false,
"business": { "name": "SmileDent Austin", "category": "dentist",
"city": "Austin", "phone": "+1…", "website": "https://…",
"email": "…", "rating": 3.9, "reviewCount": 42 },
"score": { "overall": 87, "bestService": "website_redesign",
"estimatedPain": "High", "contactConfidence": "High", "reasons": […] },
"audit": { "mobileScore": 38, "desktopScore": 51, "seoScore": 64,
"hasBookingLink": false, "hasContactForm": true, "cms": "WordPress" },
"reportUrl": "https://tellsign.app/reports/…" }
] }On a free trial, responses are gated the same way the app is: leads you haven't unlocked return phone/website/email: null, and your top-ranked leads come back as { "topLocked": true, "business": null } with only the score visible. Upgrade to receive full contact data.
Webhooks
Add an endpoint URL in Settings → Webhooks and choose which events it should receive. We POST a JSON payload to that URL whenever a subscribed event fires.
Events:
scan_complete— a campaign scan finished (payload includescampaignId,discovered,kept,newCount).new_hot_lead— high-scoring leads (70+) were found.report_viewed— a prospect opened a white-label report.rule_fired— one of your workflow automations ran.inbound_lead— someone submitted your embedded audit widget.lead_assigned·task_assigned— a teammate was assigned a lead or task.due_scan— a scheduled re-scan is due.
Delivery format
POST <your endpoint>
Content-Type: application/json
X-Lof-Event: scan_complete
X-Lof-Timestamp: 1718200000
X-Lof-Webhook-Id: evt_b3f1… # unique per delivery — dedupe on this
X-Lof-Signature: t=1718200000,v1=9a2f… # HMAC-SHA256, see below
{
"id": "evt_b3f1…",
"event": "scan_complete",
"sentAt": "2026-06-14T09:00:00.000Z",
"data": {
"kind": "scan_complete",
"title": "Scan complete · dentists — Austin",
"body": "25 ranked leads · 11 new",
"href": "/campaigns/cmq…",
"campaignId": "cmq…", "discovered": 60, "kept": 25, "newCount": 11
}
}Verifying the signature
Each endpoint has its own secret (whsec_…). The signature is HMAC-SHA256(secret, "{t}.{raw body}") — binding the timestamp to the body so old deliveries can't be replayed. To verify: parse t and v1 from X-Lof-Signature, reject if t is more than 5 minutes old, recompute the HMAC over the raw request body, and compare in constant time.
import crypto from "node:crypto";
function verify(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((kv) => kv.split("="))
);
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // > 5 min old
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(parts.v1), Buffer.from(expected)
);
}Delivery guarantees
Webhooks are best-effort: each delivery has a 6-second timeout and is attempted once (no automatic retries yet), so respond 2xx quickly and do heavy work asynchronously. Endpoints must be public HTTPS URLs. Recent delivery status and the last error are shown next to each endpoint in Settings.
Questions or need higher limits? Email [email protected].