Resources
Everything other than analytics queries. Each section maps to one API area; the typed return objects are described inline. Add ->asRaw() to any resource to get arrays instead.
Session actions
Section titled “Session actions”Two writes against a single visitor session: a custom event, and a patch of acquisition data. Both need the visitor’s session id, which only the browser knows. Session handoff explains how it reaches your backend; fromRequest() below reads it back.
// From a Request - the SDK resolves the session id$event = $snipform->session()->event($request, [ 'name' => 'purchase', 'value' => 9900, // optional 'meta' => ['order_id' => 'X-1', 'currency' => 'USD'], // optional]);
// With an id you stored earlier$event = $snipform->session()->event([ 'session_id' => $order->snipform_session_id, 'name' => 'order_shipped',]);
$event->id; // string$event->sessionId; // string$event->name; // 'purchase'$event->value; // ?string$event->meta; // array$event->createdTs; // ?int, unix secondsThrows MissingSessionIdException when a Request carries no resolvable id. Call fromRequest() first if you would rather branch than catch.
Acquisition
Section titled “Acquisition”Only the keys you send are touched. Tags merge with existing tags and are de-duplicated. Each value is recorded as an acquisition event and the session’s value is the roll-up of those events; tag the order as <source>:order:<id> to make a repeat call for the same order update it instead of adding to it. Money is integer cents. A cost key is accepted but ignored: session cost comes from cost entries and connected ad platforms.
$snipform->session()->acquisition($request, [ 'value' => 9900, // integer cents 'currency_code' => 'USD', // ISO 4217, defaults to the session's currency, else USD 'tags' => ['affiliate', 'shop:order:10421'],]);
$snipform->session()->acquisition(['session_id' => $sessionId, 'value' => 9900]);// returns ['id' => ..., 'acquisition_meta' => [...], 'value' => [...]]Resolving the session id
Section titled “Resolving the session id”$sessionId = $snipform->session()->fromRequest($request); // string|nullfromRequest() checks, in order:
- The server session, under
snipform_session_id(stashed by the Laravel hydrate loader on an earlier request). - The
X-SnipForm-Session-Idheader (set by the tracker’ssignals.attachToFetch()). - The
snip_session_idform field (set bysignals.attachToForm()). - The
snip_session_idquery parameter.
Any Symfony or Laravel Request works; the class constants Session::SESSION_HEADER, Session::SESSION_FORM_FIELD and Session::SESSION_STASH_KEY name the three transports.
Store it, fire later
Section titled “Store it, fire later”Capture the id while the visitor is on the page, keep it on your own record, and write against it from anywhere afterwards:
// Checkout controller - the visitor is present, so the id resolvespublic function store(Request $request, \SnipForm\Client $snipform){ $order = Order::create([ // ... 'snipform_session_id' => $snipform->session()->fromRequest($request), // may be null ]);
if ($order->snipform_session_id) { $snipform->session()->event($request, ['name' => 'purchase', 'value' => $order->total_cents]); $snipform->session()->acquisition($request, [ 'value' => $order->total_cents, 'currency_code' => $order->currency, ]); }}
// Fulfilment job, days later - no request, just the stored idpublic function handle(\SnipForm\Client $snipform){ if ($this->order->snipform_session_id) { $snipform->session()->event([ 'session_id' => $this->order->snipform_session_id, 'name' => 'order_shipped', ]); }}Contacts
Section titled “Contacts”A contact is a known person. The contact holds the PII; sessions reference it by id only, and a session links to a contact once. Background and lifecycle stages: Contacts.
Identify
Section titled “Identify”Find-or-create by external_id (preferred) or email, and link the session if one is supplied.
$contact = $snipform->contacts()->identify([ 'external_id' => 'usr_123', 'session_id' => $sessionId, // optional - the link only happens when present 'traits' => [ 'first_name' => 'Jane', 'company' => 'Acme', 'lifecycle_stage' => 'customer', 'meta' => [['key' => 'plan', 'value' => 'pro']], ],]);At least one of external_id or email is required (the API returns 422 otherwise). Accepted trait keys: first_name, last_name, phone, company, job_title, website, country, city, lifecycle_stage, and meta as a list of {key, value} rows.
Identify is idempotent on the server: repeat calls with the same payload merge in place. To skip the round-trip as well, wire a dedup gate on the client and use identifyOnce():
// Any atomic add-if-missing works: Cache::add in Laravel, SETNX in Redis$snipform->withIdentifyDedup( fn (string $key, int $ttl): bool => \Illuminate\Support\Facades\Cache::add($key, true, $ttl), ttl: 3600,);
$fired = $snipform->contacts()->identifyOnce([...]); // bool$fired = $snipform->contacts()->identifyFromRequest($request, [...]); // merges the tracker header as session_idThe fingerprint is sha1 of the session id plus the sorted payload, so a changed email or a new trait fires again. passesDedup($payload) runs the gate without sending, when you want to decide for yourself.
Read, update, delete
Section titled “Read, update, delete”$contact = $snipform->contacts()->find($id);
foreach ($snipform->contacts()->all(['search' => 'acme']) as $contact) { ... } // paginated$snipform->contacts()->all(['state' => 'active', 'lifecycle_stage' => 'lead'])->page(1);
foreach ($snipform->contacts()->sessionsFor($id) as $session) { ... } // SessionRow, cursor-walked
$snipform->contacts()->update($id, ['lifecycle_stage' => 'customer', 'company' => 'Acme Ltd']);
$snipform->contacts()->delete($id); // boolFilters for all(): search, state, lifecycle_stage, per_page. update() accepts every contact field plus state and meta; updating a deleted contact returns a 410 ApiException.
Delete is redaction in place, not a row drop: every identifying field is destroyed, related identify events are scrubbed, and the contact’s sessions keep only an anonymous reference. This is how an erasure request is honoured. See Contacts.
Contact fields: id, externalId, email, firstName, lastName, fullName, phone, company, jobTitle, website, country, city, lifecycleStage, state, meta, firstSeenTs, identifiedTs, lastSeenTs, sessionCount.
Conversions
Section titled “Conversions”Two surfaces: definition CRUD, and an analytics reader opened with ->for($id). Concepts are covered in Conversions.
Schema
Section titled “Schema”Ask the server for the valid vocabulary before building a config:
$schema = $snipform->conversions()->schema();
$schema['conversion_types']; // e.g. lead, sale, signup, activation, download, custom$schema['trigger_types']; // per trigger: kind, defaults, field and match options$schema['cycle_intervals']; // day, week, month$schema['segment_dimensions']; // fields you can segment by$schema['page_match_modes']; // contains, exact, starts_with, regex$schema['event_value_match_modes']; // exists, equals, gt, gte, lt, lteCreate with the builder
Section titled “Create with the builder”$conversion = $snipform->conversions()->create() ->name('Newsletter signup') ->description('Free trial sign-up flow') ->type('lead') ->conversionValue(5.00) ->defaultPeriod('last_28') ->defaultCycle('week')
->step('Visit pricing')->onPageView('/pricing') ->step('Click signup')->onEvent('signup_click') ->step('Submit form')->onFormSubmit($snipFormId)
->publish() // omit to leave it as a draft ->save(); // ConversionEach step() returns a step builder; its on*() method commits the step and hands back the conversion builder.
| Step terminal | Trips when |
|---|---|
onPageView($value, $match = 'contains', $field = 'path') | The session views a matching page. $field is path or url |
onEntryPage($value, $match = 'contains', $field = 'entry_path') | The session entered on a matching page. $field is entry_path or entry_url |
onEvent($name, $value = null, $valueMatch = 'exists') | A custom event fires, optionally gated on its value |
onFormSubmit($snipFormId) | A specific form is submitted |
onShortLink($id, $scope = 'link') | The session arrived through a short link, or any link in a group ($scope = 'group') |
Chain ->optional() before the terminal to mark a step not required. Other builder setters: valueFromEvent() (take the value from the final event instead of conversionValue), startingFrom('2026-01-01') (never count sessions before a date).
Read and patch
Section titled “Read and patch”$all = $snipform->conversions()->all(); // Conversion[]$c = $snipform->conversions()->find($id); // Conversion with ->steps populated
$c->isDraft();$c->isActive();
$snipform->conversions()->update($id, ['name' => 'Renamed', 'conversion_value' => 12.5]);
$snipform->conversions()->replaceSteps($id, [ ['name' => 'Visit', 'trigger_type' => 'page_view', 'trigger_config' => ['type' => 'page', 'field' => 'path', 'match' => 'contains', 'value' => '/pricing']], ['name' => 'Buy', 'trigger_type' => 'event', 'trigger_config' => ['type' => 'event', 'name' => 'purchase', 'valueMatch' => 'exists']],]);
$snipform->conversions()->publish($id); // draft to active (needs at least one step)$snipform->conversions()->toggle($id); // active to paused and back$snipform->conversions()->delete($id); // boolConversion fields: id, name, description, type, state, conversionValue, valueFromEvent, stepsCount, startingFrom, defaultPeriod, defaultCycle, steps (ConversionStep[]: id, name, order, triggerType, triggerConfig, isRequired).
Analytics
Section titled “Analytics”->for($id) opens a reader. Set the window with unix timestamps, optionally filter, then call a terminal.
$reader = $snipform->conversions()->for($id) ->between(strtotime('-30 days'), time()) // or ->since(strtotime('-30 days')) ->filter(['channel_category' => 'paid_search']); // optional
$summary = $reader->summary();$summary->sessions; // int - entered the funnel$summary->conversions; // int$summary->rate; // float, 0-100$summary->value; // ?float - attributed value$summary->windowFrom; // int$summary->windowTo; // int$summary->funnel; // FunnelStep[]: stepId, name, order, count, dropOff, isConversion, triggerLabel, triggerSummarySegment by a flat dimension or by a session tag key:
$reader->segments('channel_category'); // ConversionSegment[]$reader->segmentsByTag('campaign_phase'); // ConversionSegment[]// ConversionSegment: value, label, sessions, converted, rate, icon, faviconCycles recompute the funnel per repeating interval, with a delta against the previous one:
$result = $reader->cycles('week', page: 0, perPage: 6);// ['cycles' => ConversionCycle[], 'has_more' => bool, 'page' => int, 'interval' => 'week']
foreach ($result['cycles'] as $cycle) { // label, fromTs, toTs, dateFrom, dateTo, isCurrent, sessions, conversions, rate, value, delta}Sessions that reached a step (or converted fully, with null):
$reader->sessionsAt($stepId, page: 1, perPage: 25);// ['sessions' => array[], 'page' => int, 'per_page' => int, 'total' => int, 'has_more' => bool]Returning a reader from a controller serialises as summary().
Short links
Section titled “Short links”Groups hold links; clicks are recorded by the redirect. Concepts: Short Links & Campaigns.
Groups
Section titled “Groups”$groups = $snipform->linkGroups()->all(); // LinkGroup[] - not paginated$group = $snipform->linkGroups()->find($id);$group = $snipform->linkGroups()->create([ 'name' => 'Spring affiliates', 'description' => 'Affiliate links for Q2', 'purpose' => 'affiliate', 'track_clicks' => true,]);$group = $snipform->linkGroups()->update($id, ['name' => 'Spring 2026', 'state' => 'archived']);$snipform->linkGroups()->delete($id); // bool - deletes the group's links tooLinkGroup: id, name, description, purpose, state, trackClicks, countLinks, countClicks, createdAt.
foreach ($snipform->links()->all() as $link) { ... } // paginatedforeach ($snipform->links()->all(['group_id' => $groupId]) as $link) { ... }
$link = $snipform->links()->find($id);$link = $snipform->links()->create([ 'group_id' => $groupId, 'destination_url' => 'https://example.com/landing', 'domain' => 'snpf.io', 'utm' => [ 'utm_source' => 'newsletter', 'utm_medium' => 'email', 'utm_campaign' => 'spring_sale', 'utm_content' => 'pub_12345', ],]);$link = $snipform->links()->update($id, ['destination_url' => 'https://example.com/v2', 'is_active' => false]);$snipform->links()->delete($id);
$link->shortUrl; // https://snpf.io/abc123$link->utm('utm_content'); // 'pub_12345' or nullLink: id, groupId, code, shortUrl, destinationUrl, domain, utm, isActive, clicks, createdTs. The domain must be one the property is allowed to use; the dashboard’s link builder lists them.
Clicks
Section titled “Clicks”Read-only. Chain filters, then all() or find().
foreach ($snipform->clicks()->forLink($linkId)->all() as $click) { ... }
$human = $snipform->clicks() ->forGroup($groupId) ->between(strtotime('-30 days'), time()) ->usersOnly() ->perPage(100) ->all();
$bots = $snipform->clicks()->botsOnly()->all()->count();$click = $snipform->clicks()->find($clickId);| Filter | Effect |
|---|---|
forLink($id) | One short link |
forGroup($id) | One group |
between($fromTs, $toTs) | Unix-timestamp range |
since($fromTs) | Open-ended range |
usersOnly() / botsOnly() | Click type |
perPage($n) | 1 to 100 |
Click: id, shortLinkId, shortLinkGroupId, clickTs, type, referrerDomain, referrerUrl, country, countryCode, region, city, device, browser, os, isBot, botName.
Attribution
Section titled “Attribution”Run the channel engine without a visitor, and fetch the UTM preset catalog. Concepts: Channels & Attribution.
Preview
Section titled “Preview”$result = $snipform->attribution()->preview([ 'utm_source' => 'whatsapp', 'utm_medium' => 'social', 'utm_campaign' => 'spring',]);
// From a full URL - UTMs parsed from the query string; explicit keys win over URL-derived ones$result = $snipform->attribution()->preview([ 'url' => 'https://example.com/landing?utm_source=tg&utm_medium=messaging',]);
// Click ids and a referrer, for cases beyond UTMs$result = $snipform->attribution()->preview([ 'click_ids' => ['gclid' => 'xyz123'], 'referrer' => 'https://www.google.com/',]);
$result->category; // e.g. 'paid_search'$result->categoryLabel; // 'Paid Search'$result->categoryColor; // ?string$result->name; // e.g. 'Google Ads'$result->source;$result->medium;$result->campaign; // ?string$result->method; // 'utm' | 'referrer' | 'click_id' | 'custom_rule' | 'direct'$result->clickId; // ?string$result->customRule; // ?string$result->isDirect();$result->isPaid();The preview runs the same code path that classifies live sessions, so what it says now is what a visitor landing with those parameters gets.
Presets
Section titled “Presets”foreach ($snipform->attribution()->presets() as $preset) { // ['group' => 'Messaging', 'key' => 'whatsapp', 'label' => 'WhatsApp', // 'utm_source' => 'whatsapp', 'utm_medium' => 'messaging']}presets() always returns the raw array; it is shaped for rendering as chips in a link builder.