← Boom Media Labs
Boom Media Labs
Internal research & playbooks · private

SureContact CRM — In-Code Lead Capture

Wire SureContact lead capture into any client project in code, so a web form, intake, or booking upserts the lead into SureContact, adds it to a list, and tags it — reliably, with no Ottokit / no-code middleman. Drop the reusable module into any repo; everything client-specific is an env var.

API verified live 2026-07-23 Portable · client-agnostic Next.js / TypeScript
Why in-code, not Ottokit? Ottokit duplicates work the app already does, its editor truncates long fields (causes Undefined array key 0), it has no update API (every change = rebuild + new webhook URL), and it fails silently. A ~120-line module you configure with env vars is reusable across every client and just works.

1 API contract (verified live)

Base URLhttps://api.surecontact.com/api/v1/public
Authheader X-API-Key: <key> (NOT Bearer)
ContentContent-Type: application/json, Accept: application/json
Docshttps://api.surecontact.com/docs
Key guidesurecontact.com/docs/surecontact-api-keys-guide

Endpoints used (verified live 2026-07-23)

ActionMethodPathBody
Upsert contactPOST/contacts/upsert{"primary_fields":{"email","first_name","last_name"}}
Add to list(s)POST/contacts/{uuid}/lists/attach{"list_uuids":["…"]}
Apply tag(s)POST/contacts/{uuid}/tags/attach{"tag_uuids":["…"]}
Remove from listPOST/contacts/{uuid}/lists/detach{"list_uuids":["…"]}
Remove tagPOST/contacts/{uuid}/tags/detach{"tag_uuids":["…"]}
Find by emailGET/contacts/email/{email}
List all listsGET/lists
List all tagsGET/tags
List contactsGET/contacts?limit=1— (auth test)
Update contactPUT/contacts/{uuid}contact fields
Delete contactDELETE/contacts/{uuid}
⚠️ The published docs page is wrong on two points Verified by live testing: (1) upsert needs identifying fields nested under primary_fields, NOT a flat {email} (flat → 422); (2) list/tag attach is /contacts/{uuid}/lists/attach + /tags/attach with array bodies (list_uuids / tag_uuids), NOT /lists/{uuid}/contacts-add. Trust this table, not the docs page. Response record UUID is at data.uuid.
Flow: upsert contact → read data.uuid → attach list(s) → attach tag(s).

Get real list/tag UUIDs from GET /lists and GET /tags (the UUIDs shown in a no-code tool like Ottokit are the same SureContact-native UUIDs).


2 Per-client setup checklist

  1. Generate a PUBLIC API key in the SureContact dashboard (see key guide link). A key from before the public API existed will 401 — you need a fresh one.
  2. Get the list UUID(s) for where leads should land (e.g. "Leads").
  3. Get the tag UUID(s) to apply (e.g. "New Lead").
  4. Set env vars (below). On managed hosts like Coolify, set these in the dashboard, not a committed .env.
  5. Verify with the curl in §5 before relying on it.

Env vars

SURECONTACT_API_KEY=          # required — the public API key
SURECONTACT_LEADS_LIST_UUID=  # list to add leads to
SURECONTACT_NEW_LEAD_TAG_UUID=# tag to apply on capture
# optional override; defaults to the public base URL:
# SURECONTACT_API_URL=https://api.surecontact.com/api/v1/public
Safe to leave wired in The module no-ops safely if SURECONTACT_API_KEY is unset, so it's safe to leave wired into a project that hasn't been set up yet.

3 The reusable module

Save as src/app/api/lib/surecontact-crm.ts (adjust path to your project). It is fire-and-forget: a CRM outage never breaks the form.

/**
 * SureContact CRM — lead capture via the public REST API.
 * Reusable across client projects. Fire-and-forget: never breaks the form.
 * API base: https://api.surecontact.com/api/v1/public  ·  auth: X-API-Key
 */
const SC_BASE =
  process.env.SURECONTACT_API_URL || "https://api.surecontact.com/api/v1/public"

function scHeaders(key: string): Record<string, string> {
  return { "X-API-Key": key, "Content-Type": "application/json", Accept: "application/json" }
}

async function scPost(key: string, path: string, body: unknown): Promise<any> {
  const res = await fetch(`${SC_BASE}${path}`, {
    method: "POST", headers: scHeaders(key), body: JSON.stringify(body),
  })
  if (!res.ok) {
    const text = await res.text().catch(() => "")
    throw new Error(`SureContact ${path} → ${res.status} ${text.slice(0, 200)}`)
  }
  return res.json().catch(() => ({}))
}

/** Upsert a contact and return its UUID. Fields go under primary_fields. */
export async function upsertSureContact(
  key: string, email: string, firstName?: string, lastName?: string
): Promise<string | null> {
  const data = await scPost(key, "/contacts/upsert", {
    primary_fields: { email, first_name: firstName || "", last_name: lastName || "" },
  })
  return data?.data?.uuid || data?.uuid || null
}

export interface SureContactLead {
  email: string
  firstName?: string
  lastName?: string
  listUuids?: string[] // defaults to SURECONTACT_LEADS_LIST_UUID
  tagUuids?: string[]  // defaults to SURECONTACT_NEW_LEAD_TAG_UUID
}

/** Upsert + add-to-list + tag. Logs and swallows every error. No-op without a key. */
export function captureSureContactLead(lead: SureContactLead): void {
  const key = process.env.SURECONTACT_API_KEY
  if (!key || !lead.email) return
  const lists = lead.listUuids ??
    ([process.env.SURECONTACT_LEADS_LIST_UUID].filter(Boolean) as string[])
  const tags = lead.tagUuids ??
    ([process.env.SURECONTACT_NEW_LEAD_TAG_UUID].filter(Boolean) as string[])

  void (async () => {
    const uuid = await upsertSureContact(key, lead.email, lead.firstName, lead.lastName)
    if (!uuid) {
      console.error("⚠️ SureContact: upsert returned no contact UUID for", lead.email)
      return
    }
    const jobs: Promise<unknown>[] = []
    if (lists.length) jobs.push(scPost(key, `/contacts/${uuid}/lists/attach`, { list_uuids: lists }))
    if (tags.length)  jobs.push(scPost(key, `/contacts/${uuid}/tags/attach`,  { tag_uuids: tags }))
    const results = await Promise.allSettled(jobs)
    results
      .filter((r): r is PromiseRejectedResult => r.status === "rejected")
      .forEach((r) => console.error("⚠️ SureContact list/tag error (ignored):", r.reason))
  })().catch((err) => console.error("⚠️ SureContact lead capture error (ignored):", err))
}

4 Wiring it into a form handler

import { captureSureContactLead } from "@/app/api/lib/surecontact-crm"

export async function POST(req: NextRequest) {
  const data = await req.json()
  const parts = String(data.name || "").trim().split(" ")

  // Fire-and-forget — do NOT await; the form must return even if the CRM is down.
  captureSureContactLead({
    email: data.email,
    firstName: parts[0],
    lastName: parts.slice(1).join(" "),
    // Omit listUuids/tagUuids to use the env defaults, or pass per-call:
    // listUuids: ["<list-uuid>"], tagUuids: ["<tag-uuid>"],
  })

  // ... rest of the handler
}

For a restaurant client, the same call captures a reservation/waitlist lead — just point the env UUIDs at that client's "Leads" list and "New Lead" tag.


5 Verify BEFORE relying on it

KEY="<paste public api key>"
# 200 + JSON = key valid.  401 API_KEY_INVALID = wrong/stale key (regenerate).
curl -s -w "\nHTTP:%{http_code}\n" -H "X-API-Key: $KEY" -H "Accept: application/json" \
  "https://api.surecontact.com/api/v1/public/contacts?limit=1"

# Upsert a test contact (note primary_fields nesting) → returns data.uuid:
curl -s -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"primary_fields":{"email":"test+sc@example.com","first_name":"Test","last_name":"Lead"}}' \
  "https://api.surecontact.com/api/v1/public/contacts/upsert"

# Get real list/tag UUIDs, then attach:
curl -s -H "X-API-Key: $KEY" "https://api.surecontact.com/api/v1/public/lists"
curl -s -H "X-API-Key: $KEY" "https://api.surecontact.com/api/v1/public/tags"

# Clean up the test contact afterward:
curl -s -X DELETE -H "X-API-Key: $KEY" \
  "https://api.surecontact.com/api/v1/public/contacts/{uuid}"

6 Gotchas


Big Mike's Bail Bonds — reference values

(For the BMBB project specifically; other clients differ.) All UUIDs below verified against the live GET /lists and GET /tags on 2026-07-23 — SureContact-native and current.

BMBB env to set (Coolify dashboard for prod):

SURECONTACT_API_KEY=<the public API key generated 2026-07-23>
SURECONTACT_LEADS_LIST_UUID=6cd7d09d-1559-42f1-a85a-4f094bedeac4
SURECONTACT_NEW_LEAD_TAG_UUID=1782ca49-7c1e-4ab9-97b0-a93bbd346042