Inbound webhooks and events API
Ingest leads from Zapier, LinkedIn Lead Gen, and other tools, track custom in-app behavioral events, and see which lead capture endpoints need the API token.
Two server-to-server endpoints bring data from other systems into Odden: the external lead webhook creates or updates contacts from lead sources such as Zapier or LinkedIn Lead Gen, and the behavioral events API records what contacts do in your product. Both need the marketing API token.
Paths below use the default API prefix, api/marketing. See public routes to change it.
Authentication
Server-to-server endpoints use the ODDEN_MARKETING_API_TOKEN shared secret (config key odden-marketing.api.token). Send it as an Authorization: Bearer header, an X-Odden-Token header, or a token query parameter for providers that only let you enter a URL. A missing or wrong token returns 401. While no token is configured, these endpoints return 403. See API tokens.
They're rate limited by odden-api (600 requests per minute per IP by default, see rate limits) and are exempt from CSRF verification.
Lead capture endpoints at a glance
| Endpoint | Route name | Token | Rate limiter | Documented in |
|---|---|---|---|---|
POST /api/marketing/leads/webhook/{source?} |
odden.marketing.leads.webhook |
Yes | odden-api |
below |
POST /api/marketing/events/track |
odden.marketing.events.track |
Yes | odden-api |
below |
POST /api/marketing/workflows/{workflow}/enroll |
odden.marketing.workflows.enroll-webhook |
Yes | odden-api |
Workflows |
POST /api/marketing/events/{slug}/attendance-webhook |
odden.marketing.events.attendance-webhook |
Yes | odden-api |
Events |
POST /api/marketing/forms/{slug} |
odden.marketing.forms.api-submit |
No | odden-public |
Forms |
POST /api/marketing/events/{slug}/register |
odden.marketing.events.register |
No | odden-public |
Events |
POST /marketing/track/pageview |
odden.marketing.track.pageview |
No | odden-public |
Web tracking |
POST /marketing/forms/auto-capture |
odden.marketing.forms.auto-capture |
No | odden-public |
Web tracking |
All of these are CSRF exempt. Email delivery webhooks (ESP bounces and complaints) are covered in deliverability.
External lead webhook
| Method | URI | Route name |
|---|---|---|
POST |
/api/marketing/leads/webhook/{source?} |
odden.marketing.leads.webhook |
{source} names the lead source, for example zapier, linkedin, or zoom. It's stored on the contact as the lead_source custom property. The segment is optional: without it (/api/marketing/leads/webhook) the source field of the payload is used, or webhook if that's missing too. A source segment takes precedence over the payload's source.
curl -X POST https://your-app.test/api/marketing/leads/webhook/linkedin \
-H "Authorization: Bearer $ODDEN_MARKETING_API_TOKEN" \
-H "Accept: application/json" -H "Content-Type: application/json" \
-d '{
"email": "Jordan@Globex.com",
"first_name": "Jordan",
"last_name": "Lee",
"company": "Globex",
"job_title": "Head of Sales",
"campaign": "q3-abm",
"properties": {"region": "EMEA"}
}'| Field | Rules |
|---|---|
email |
Required, valid email, max 255. |
first_name, last_name, company, job_title, campaign |
Optional strings, max 255. |
phone |
Optional string, max 50. |
properties |
Optional object, merged into the contact's custom properties. |
{
"success": true,
"contact_id": 31,
"is_new": true,
"lead_score": 15,
"enrolled_workflows": 0,
"message": "Lead successfully ingested into Odden CRM."
}Odden\Marketing\Actions\IngestExternalLeadAction processes the lead in a database transaction:
- Loads the contact by lowercased email, or creates one with
lifecycle_stagelead,lead_statusnew, and a score of 0. - Overwrites
first_name,last_name,phone, andjob_titlewith any non-empty values from the payload. Unlike form submissions, existing values are replaced. - Sets the custom properties
lead_source(the source) andlead_campaign(thecampaignfield), mergespropertieson top, and setslast_contacted_atto now. - If
companyis given, loads the company with that exact name or creates it with the email's domain asdomain, and associates it with the contact asprimary. - Applies the
form_submissionscoring event (15 points by default), described asIngested via {source}. - Enrolls the contact in every active
form_submittedworkflow. The workflows'trigger_config.form_idisn't checked, so every such workflow receives external leads.
enrolled_workflows is the number of active form_submitted workflows, including any the contact was already enrolled in or that have no steps. lead_score is the score after step 5.
You can call the action directly, for example from an import job. email is the only required key:
use Odden\Marketing\Actions\IngestExternalLeadAction;
$result = app(IngestExternalLeadAction::class)->execute([
'email' => 'jordan@globex.com',
'first_name' => 'Jordan',
'source' => 'csv-import',
'properties' => ['region' => 'EMEA'],
]);
$result['contact']; // Contact
$result['is_new']; // bool
$result['lead_score']; // int
$result['enrolled_workflows_count']; // intCustom behavioral events
Track what contacts do in your product (created a project, invited a teammate, hit a usage limit) to score them and trigger workflows.
| Method | URI | Route name |
|---|---|---|
POST |
/api/marketing/events/track |
odden.marketing.events.track |
curl -X POST https://your-app.test/api/marketing/events/track \
-H "Authorization: Bearer $ODDEN_MARKETING_API_TOKEN" \
-H "Accept: application/json" -H "Content-Type: application/json" \
-d '{"event_name": "project_created", "email": "builder@example.com", "properties": {"plan": "pro", "projects": 3}}'| Field | Rules |
|---|---|
event_name |
Required string, max 255. |
contact_id |
Optional number. Takes precedence over email when the contact exists. |
email |
Optional valid email. Used to find the contact, or create it, when there's no contact_id match. |
properties |
Optional object, stored with the event. |
{
"success": true,
"event_id": 1,
"event_name": "project_created",
"contact_id": 12,
"company_id": null,
"message": "Behavioral event tracked and processed successfully."
}Odden\Marketing\Actions\TrackCustomBehavioralEventAction stores the event in Odden\Marketing\Models\CustomBehavioralEvent (contact_id, company_id, event_name, properties, occurred_at). An email without a matching contact creates one (lead / new) and runs lead-to-account matching on it. company_id is the contact's first associated company.
With a contact, the action also:
- logs a
Custom Event: {event_name}task with the properties as JSON; - applies the
custom_eventscoring event (5 points by default, or the score of an activecustom_eventrule) on every event; - enrolls the contact in active
custom_eventworkflows whosetrigger_config.event_namematches (case-insensitively) or is unset; - recalculates the company's intent if there's a company.
Without contact_id or email, the event is stored anonymously and nothing else happens.
From PHP, for example in an event listener in your app:
use Odden\Marketing\Actions\TrackCustomBehavioralEventAction;
$event = app(TrackCustomBehavioralEventAction::class)->execute(
eventName: 'project_created',
contact: $contact,
properties: ['source' => 'onboarding'],
);The signature is execute(string $eventName, ?Contact $contact = null, ?string $email = null, array $properties = [], ?CarbonInterface $occurredAt = null): CustomBehavioralEvent. $occurredAt defaults to now.
The package adds a customBehavioralEvents relation to Contact. Events are also counted by the conversion funnel.