Opt-Ins & Submit API Reference

POST external lead submission data directly into Hermon, and query historical traffic records, UTM attributions, logs, and form submissions.

Date & Timezone Handling: All timestamps in responses are UTC, formatted as ISO 8601 (2026-01-15T10:30:00.000Z). Date-range filter parameters accept either YYYY-MM-DD (which expands to UTC day boundaries) or full ISO 8601 timestamps.
Both bounds are inclusive. Only created_at is filterable on the list endpoint (there is no updated_at on opt-ins).

Submit Endpoint

POST/api/opt-in/submit

Accepts both full-access and opt-in only API keys. Rate limited to 5 requests per second per API key. On a 429, wait the seconds in the Retry-After header and retry with the same external_reference.

Headers

Content-Type:application/json
x-api-key:sk_live_your_key_here

Request Body Fields

FieldTypeRequiredDescription
emailstringRequiredLead's email address
first_namestringOptionalLead's first name. If omitted, a new lead's first name is derived from the email prefix; an existing lead's name is never overwritten by an omitted value
sourcestringRequiredOne of: CALENDLY, ICLOSED, GOHIGHLEVEL, MANUAL, TYPEFORM, WEBINAR, NEWSLETTER, LANDING_PAGE, OTHER
last_namestringOptionalLead's last name
phone_e164stringOptionalPhone in E.164 format (e.g. +14155552671)
phone_countrystringOptionalISO country code. phone_e164 and phone_country must be sent together
provider_form_idstringOptionalYour form's unique identifier
provider_form_namestringOptionalYour form's display name
external_referencestringOptionalYour unique submission ID for idempotency (unique per source)
questions_answersarrayOptionalQ&A pairs from your form (max 50). Each item: question (string, required, max 1000 chars), answer (string, required, max 5000 chars), position (integer, optional — defaults to the item index)
trafficobjectOptionalUTM parameters, referrer, landing page, Meta click IDs (fbclid, fbc, fbp) and the tracking snippet public_key. Each value max 500 chars
user_agentstringOptionalVisitor's browser user agent (the tracking snippet's hermon_ua field). Max 1000 chars. When omitted, the request's User-Agent header is used
companyobjectOptionalCompany details saved on the lead. Every field inside is optional; see the company object

questions_answers item

json
{
  "question": "What is your biggest challenge?",
  "answer": "Generating consistent leads",
  "position": 0
}

traffic object

json
{
  "utm_source": "facebook",
  "utm_medium": "paid",
  "utm_campaign": "q1-coaching",
  "utm_content": "video-ad-1",
  "utm_term": "coaching program",
  "referrer": "https://facebook.com",
  "landing_page": "https://yoursite.com/apply?fbclid=IwAR0abc123",
  "fbclid": "IwAR0abc123",
  "fbc": "fb.1.1736937000000.IwAR0abc123",
  "fbp": "fb.1.1736937000000.1098115397",
  "public_key": "pk_live_0123456789abcdef0123456789abcdef"
}
If traffic.utm_content starts with user_ (a Clerk user ID), Hermon automatically assigns that user to the lead — as the triager if they hold the Triager role, otherwise as the setter. This enables automated attribution from ad tracking.
Meta click identifiers (fbclid / fbc / fbp). Forward these inside traffic the same way you forward UTMs, so Hermon can later match conversions back to the originating ad click. All three are optional and a lead is never rejected for lacking them.
  • fbclid — the raw Meta click ID from the ad URL. If you do not send it explicitly but landing_page contains an fbclid query parameter, Hermon extracts it from that URL.
  • fbc — the value of the Meta _fbc cookie. When sent it is stored as-is. When only fbclidis available, Hermon builds it in Meta's required fb.1.<timestamp_ms>.<fbclid> format using the opt-in time.
  • fbp — the value of the Meta _fbp cookie, stored as submitted.
IP address & user agent. Hermon records the IP from the request that submits the opt-in: the X-Forwarded-For header (first entry), falling back to the connection IP. The user agent is taken from the user_agent body field when you send it, otherwise from the User-Agent header. When you submit server-side (Zapier, Make, your backend), the header describes your server, not the visitor — forward the tracking snippet's hermon_ua value as user_agentso the visitor's browser is recorded.
traffic.public_key — the tracking snippet's hermon_pk value. Optional. When it is sent and does not belong to the workspace of your API key (or is unknown), the lead is still created but the whole traffic object is ignored. When it is omitted, traffic is accepted as before.

company object

Company details submitted alongside the opt-in. They are saved on the lead and appear in its Company Details panel; they are not stored separately on the opt-in record. The whole object is optional, so existing integrations are unaffected.

json
{
  "business_name": "Elektrotechnisch Bureau Takken",
  "vat_number": "NL123456789B01",
  "address": "Voorbeeldstraat 1",
  "city": "Amsterdam",
  "zip_code": "1011AB",
  "state_province": "Noord-Holland",
  "country": "NL"
}
FieldTypeRequiredDescription
business_namestringOptionalRegistered company name. Max 255 characters.
vat_numberstringOptionalVAT number, stored as submitted (trimmed and uppercased). Not format-checked and not validated against VIES. Max 50 characters.
addressstringOptionalStreet address. Max 255 characters.
citystringOptionalCity. Max 100 characters.
zip_codestringOptionalZIP / postal code. Max 20 characters.
state_provincestringOptionalState / province. Max 100 characters.
countrystringOptionalISO 3166-1 alpha-2 country code (e.g. NL, US). Case-insensitive; an unknown code is rejected with a 400.
Partial data is accepted. No field inside company is required — only the format of what you send is checked. Send just a business_name and vat_number if that is all your form collects. An object whose fields are all empty is ignored and no company record is created.
Merge behaviour: empty fields only. A value already stored on the lead is never overwritten by an opt-in. Each field you send fills the matching company field only when it is currently empty; the rest are ignored. This applies identically to a newly created lead and to an opt-in matched to an existing lead (is_new_lead: false). To correct a value that is already set, edit the lead's Company Details in Hermon.

Company details are also ignored on an idempotent replay — when the same external_reference + source is submitted again, Hermon returns the existing opt-in without applying any field from the repeat request.

Request example with company details

json
{
  "email": "info@etbtakken.nl",
  "first_name": "Jan",
  "source": "LANDING_PAGE",
  "company": {
    "business_name": "Elektrotechnisch Bureau Takken",
    "vat_number": "NL123456789B01",
    "address": "Voorbeeldstraat 1",
    "city": "Amsterdam",
    "zip_code": "1011AB",
    "state_province": "Noord-Holland",
    "country": "NL"
  }
}

Submit Responses

201New lead created via opt-in
json
{
  "status": "success",
  "message": "Lead created via opt-in",
  "data": {
    "opt_in_id": "uuid",
    "lead_id": "uuid",
    "is_new_lead": true
  }
}
201Opt-in recorded for an existing lead (or idempotent replay)
json
{
  "status": "success",
  "message": "Opt-in recorded for existing lead",
  "data": {
    "opt_in_id": "uuid",
    "lead_id": "uuid",
    "is_new_lead": false
  }
}
401Missing or invalid API key
json
{
  "status": "error",
  "message": "Invalid or revoked API key"
}
400Validation failure
json
{
  "status": "error",
  "message": "source must be one of: CALENDLY, ICLOSED, GOHIGHLEVEL, MANUAL, TYPEFORM, WEBINAR, NEWSLETTER, LANDING_PAGE, OTHER"
}

Company details are validated the same way — an unknown country code or a wrong type names the offending field. Nothing is written when validation fails: no lead, opt-in or company data is created.

json
{
  "status": "error",
  "message": "company.country must be an ISO 3166-1 alpha-2 country code (e.g. NL, US)"
}
429Rate limit exceeded
json
{
  "status": "error",
  "message": "API key rate limit exceeded. Maximum 5 requests per second."
}
409external_reference already used by a different lead
json
{
  "status": "error",
  "message": "This submission reference is already linked to a different lead. If this is a new lead, resubmit without an external_reference or with a unique one."
}

Idempotency

Pass external_reference with a unique ID from your system. If the same external_reference + source combination is submitted again, Hermon returns the existing opt-in record without creating a duplicate. If that external_reference is already linked to a lead with a different email, the request is rejected with 409.

json
{
  "email": "john@example.com",
  "first_name": "John",
  "source": "LANDING_PAGE",
  "external_reference": "typeform-submission-abc123"
}

List Opt-Ins Records

List Opt-Ins

Retrieve a paginated list of opt-ins. Results can be filtered by source, lead ID, and creation date.

GET/api/external/opt-ins

Query Parameters

ParameterTypeRequiredDescription
pageintegerOptionalPage number for pagination. Default: 1
limitintegerOptionalNumber of items per page. Default: 10, Max: 100
sourcestringOptionalFilter by opt-in source. See Opt-in Source Values.
lead_idstring (UUID)OptionalUUID of a specific lead. Returns opt-ins linked to that lead.
created_fromstringOptionalLower bound for created_at. Accepts YYYY-MM-DD or ISO 8601.
created_tostringOptionalUpper bound for created_at. Accepts YYYY-MM-DD or ISO 8601.

Code Examples

curl -X GET "https://api.hermon.io/api/external/opt-ins?page=1&limit=10&source=CALENDLY&lead_id=e799c30f-9da7-4fd7-9552-83b96a9bfac2" \
  -H "Content-Type: application/json" \
  -H "x-api-key: sk_live_your_key_here"

Response Example

json
{
  "status": "success",
  "message": "Opt-ins fetched successfully",
  "data": [
    {
      "id": "optin_e5f6a7b8-c9d0-1234-efgh-345678901234",
      "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "source": "TYPEFORM",
      "provider_form_id": "form_abc123def456",
      "provider_form_name": "Coaching Application Form",
      "external_reference": "TYPEFORM-response-abc123",
      "created_at": "2026-01-15T10:30:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "totalPages": 1,
    "hasNext": false,
    "hasPrev": false
  }
}

Get Opt-In Details

Fetch detailed information for a single opt-in submission record by its unique UUID.

GET/api/external/opt-ins/:id

Path Parameters

:idRequired. The unique opt-in UUID.

Opt-In Object Fields

FieldTypeDescription
idstring (UUID)Unique identifier for the opt-in.
lead_idstring (UUID)ID of the lead this opt-in created or matched.
sourcestring (enum)The system or integration that captured the opt-in. See Opt-in Source Values.
provider_form_idstring | nullThe form's identifier in the originating provider (e.g. Typeform form ID). null for manual entries.
provider_form_namestring | nullThe form's display name in the originating provider. null for manual entries.
external_referencestring | nullThe provider's unique reference for this specific submission (e.g. Typeform response ID).
created_atstring (ISO 8601)UTC timestamp when the opt-in was captured.
question_answersarrayOrdered list of form question/answer pairs. See Question Answer Object [Details API Only].
traffic_attributionobjectUTM, referrer, and Meta click ID data. See Traffic Attribution Object [Details API Only].

Code Examples

curl -X GET "https://api.hermon.io/api/external/opt-ins/522b1759-d354-472c-8fb2-1a9bbc69eb9b" \
  -H "Content-Type: application/json" \
  -H "x-api-key: sk_live_your_key_here"

Response Example

json
{
  "status": "success",
  "message": "Opt-in fetched successfully",
  "data": {
    "id": "optin_e5f6a7b8-c9d0-1234-efgh-345678901234",
    "lead_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "source": "TYPEFORM",
    "provider_form_id": "form_abc123def456",
    "provider_form_name": "Coaching Application Form",
    "external_reference": "TYPEFORM-response-abc123",
    "created_at": "2026-01-15T10:30:00.000Z",
    "question_answers": [
      {
        "id": "qa_001",
        "question": "What is your biggest business challenge right now?",
        "answer": "Struggling to convert leads into high-ticket clients consistently",
        "position": 1
      },
      {
        "id": "qa_002",
        "question": "What is your monthly revenue?",
        "answer": "$10,000 - $25,000",
        "position": 2
      },
      {
        "id": "qa_003",
        "question": "How did you hear about us?",
        "answer": "Facebook Ad",
        "position": 3
      }
    ],
    "traffic_attribution": {
      "utm_source": "facebook",
      "utm_medium": "paid_social",
      "utm_campaign": "q1-2026-coaching",
      "utm_content": "video-ad-v2",
      "utm_term": null,
      "referrer": "https://www.facebook.com/",
      "landing_page": "https://example.com/apply?fbclid=IwAR0abc123",
      "fbclid": "IwAR0abc123",
      "fbc": "fb.1.1736937000000.IwAR0abc123",
      "fbp": "fb.1.1736937000000.1098115397"
    }
  }
}

Values Reference

Opt-in Source Values

ValueDescription
CALENDLYCalendly form/event
ICLOSEDiClosed integration
GOHIGHLEVELGoHighLevel form
MANUALManually created
TYPEFORMTypeform submission
WEBINARWebinar registration
NEWSLETTERNewsletter signup
LANDING_PAGELanding page form
OTHEROther source

Nested Objects Reference

Question Answer Object Fields

FieldTypeDescription
idstringUnique identifier for this question/answer pair.
questionstringThe form question text.
answerstringThe respondent's answer.
positionintegerDisplay order, ascending. Opt-ins submitted without a position use the 0-based item index.

Traffic Attribution Object Fields

FieldTypeDescription
utm_sourcestring | nullUTM source (e.g. "facebook", "google").
utm_mediumstring | nullUTM medium (e.g. "paid_social", "email").
utm_campaignstring | nullUTM campaign name.
utm_contentstring | nullUTM content variant.
utm_termstring | nullUTM keyword term.
referrerstring | nullHTTP referrer URL (the page that linked to the form).
landing_pagestring | nullURL of the landing page the visitor was on when they submitted the form.
fbclidstring | nullMeta click ID captured at opt-in. Taken from the fbclid field, or extracted from the fbclid query parameter of landing_page when not sent explicitly.
fbcstring | nullMeta click cookie value in fb.1.<timestamp_ms>.<fbclid> format. Stored as-is when the _fbc cookie is forwarded; otherwise built from fbclid using the opt-in time.
fbpstring | nullMeta browser ID (the _fbp cookie value), stored as submitted.

Management Errors

400Invalid or unknown query parameter
json
{
  "status": "error",
  "message": "Unknown query parameter(s): status"
}
401Missing or invalid API key
json
{
  "status": "error",
  "message": "Invalid or revoked API key"
}
403Opt-in only API key used on a read endpoint
json
{
  "status": "error",
  "message": "This API key is restricted to submitting opt-ins and cannot access this resource."
}
404Resource not found
json
{
  "status": "error",
  "message": "Resource not found"
}
429Rate limit exceeded
json
{
  "status": "error",
  "message": "Rate limit exceeded for this organization. Retry after the number of seconds in the Retry-After header."
}