Custom HTML
A hand-built HTML form is the most flexible case: you own the markup, so you can either add hidden inputs the tag fills, or read the resolved values in JavaScript and attach them to whatever you submit. No form plugin required — a [mtk:…]token in an input's valueis all the tag needs, and the input's name is yours to choose.
Load the tag on every page
Paste the loader into your page's <head>, or add it as a Custom HTML tag in Google Tag Manager firing on all pages.
<script src="https://attr.boggsmtk.com/v1/tag.js?k=YOUR_LICENSE_KEY"></script>
Swap YOUR_LICENSE_KEYfor the client's key from the dashboard. On page load the tag resolves attribution, pushes it to the mtk_attribution dataLayer event, and fills any matching hidden field.
Get the values onto the lead — pick the method that fits your form
Two interchangeable approaches. Use the first for a form that POSTs natively; use the second when your form is JS-built and sends its own request (fetch/XHR to an API, CRM, or Supabase).
A · Hidden inputs — add one hidden <input> per value: name it whatever you want the submission row to be called (its Field Name reads well in lead emails, e.g. First Channel: Paid Search), and put the field's Token in value. The tag fills by value — replacing the token on page load and again at submit — so the data rides along in the form body and lands wherever you store submissions. The block below is the complete default field map — keep the rows you want and drop the rest; confirm names and tokens against Dashboard → license → Tag config → Field map.
Paste it as one block, <div> included. Hidden inputs draw nothing on their own, but any editor that reformats your markup on save will treat these as ordinary lines: WordPress runs the markup through wpautop, which turns every newline between the inputs into a <br> and every blank-line group into a <p>, opening a gap the height of the whole block. The display:none wrapper collapses those injected breaks along with everything else inside it, so the same block is safe to paste anywhere. It changes nothing about filling — the tag matches on the token in value, not on whether the field is visible.
<div style="display:none" aria-hidden="true">
<!-- The [mtk:…] token in value= is what the tag fills — the name is
yours, so submissions read nicely. Keep the rows you want. -->
<!-- Query sources -->
<input type="hidden" name="First Ad Placement" value="[mtk:first.ad_placement]" />
<input type="hidden" name="Last Ad Placement" value="[mtk:last.ad_placement]" />
<input type="hidden" name="First Campaign" value="[mtk:first.campaign]" />
<input type="hidden" name="Last Campaign" value="[mtk:last.campaign]" />
<input type="hidden" name="First Channel" value="[mtk:first.channel]" />
<input type="hidden" name="Last Channel" value="[mtk:last.channel]" />
<input type="hidden" name="First Content" value="[mtk:first.content]" />
<input type="hidden" name="Last Content" value="[mtk:last.content]" />
<input type="hidden" name="First Landing Page" value="[mtk:first.landing_page]" />
<input type="hidden" name="Last Landing Page" value="[mtk:last.landing_page]" />
<input type="hidden" name="First Landing Page Group" value="[mtk:first.landing_page_group]" />
<input type="hidden" name="Last Landing Page Group" value="[mtk:last.landing_page_group]" />
<input type="hidden" name="First Medium" value="[mtk:first.medium]" />
<input type="hidden" name="Last Medium" value="[mtk:last.medium]" />
<input type="hidden" name="First Query" value="[mtk:first.query]" />
<input type="hidden" name="Last Query" value="[mtk:last.query]" />
<input type="hidden" name="First Referrer" value="[mtk:first.referrer]" />
<input type="hidden" name="Last Referrer" value="[mtk:last.referrer]" />
<input type="hidden" name="First Source" value="[mtk:first.source]" />
<input type="hidden" name="Last Source" value="[mtk:last.source]" />
<input type="hidden" name="First Term" value="[mtk:first.term]" />
<input type="hidden" name="Last Term" value="[mtk:last.term]" />
<!-- Cookies & Click IDs -->
<input type="hidden" name="gclid" value="[mtk:last_paid.gclid]" />
<input type="hidden" name="gbraid" value="[mtk:last_paid.gbraid]" />
<input type="hidden" name="wbraid" value="[mtk:last_paid.wbraid]" />
<input type="hidden" name="msclkid" value="[mtk:last_paid.msclkid]" />
<input type="hidden" name="fbclid" value="[mtk:param:fbclid]" />
<input type="hidden" name="fbc" value="[mtk:cookie:_fbc]" />
<input type="hidden" name="fbp" value="[mtk:cookie:_fbp]" />
<!-- Insights -->
<input type="hidden" name="Journey String" value="[mtk:journey.string]" />
<input type="hidden" name="Journey JSON" value="[mtk:journey.json]" />
<input type="hidden" name="Session Count" value="[mtk:session.count]" />
<input type="hidden" name="Pageview Count" value="[mtk:pageview.count]" />
<input type="hidden" name="Paid Touch Count" value="[mtk:paid_touch_count]" />
<input type="hidden" name="Last Paid Touch" value="[mtk:last.paid_touch]" />
<input type="hidden" name="Time Since Last Paid Touch" value="[mtk:time_since_last_paid_touch]" />
<input type="hidden" name="Page Path" value="[mtk:page.path]" />
<input type="hidden" name="Page URL" value="[mtk:page.url]" />
<input type="hidden" name="Time To Conversion" value="[mtk:time_to_conversion]" />
<input type="hidden" name="Conversion Unix" value="[mtk:conversion_unix]" />
<input type="hidden" name="Conversion Datetime" value="[mtk:conversion_datetime]" />
<input type="hidden" name="Custom Conversion Time" value="[mtk:custom_conversion_time]" />
<input type="hidden" name="Tracking Health" value="[mtk:detect:tracking_health]" />
<input type="hidden" name="Attribution Summary" value="[mtk:attribution_summary]" />
</div>Naming an input its HTML Name (e.g. mtk_first_source) works too — the tag also fills any field whose namematches the field map, no token needed — and that's what Auto-create missing fieldsin the tag config injects for you if you'd rather not hand-add inputs at all. One trade-off to know: if the tag never loads (ad blocker, script error), a token field submits its raw [mtk:…] text, whereas an mtk_*-named field simply submits empty. Mixing both styles in one form works fine.
B · Read the values in JavaScript — if you build the submit payload yourself, read the resolved values off the mtk_attributiondataLayer event and merge them into the record you send. Attribution is keyed to the browser, so there's no identifier to pass — just fold in the fields.
// Read the attribution values the tag resolved and pushed on page load.
// Keys are your field-map input names (mtk_first_source, mtk_last_channel, …).
function mtkFields() {
var dl = window.dataLayer || [];
var evt = null;
for (var i = dl.length - 1; i >= 0; i--) {
if (dl[i] && dl[i].event === "mtk_attribution") { evt = dl[i]; break; }
}
var out = {};
if (evt) Object.keys(evt).forEach(function (k) {
if (k.indexOf("mtk_") === 0) out[k] = evt[k];
});
return out;
}
// At your confirmed-successful-submit point — the same place you already
// know the submit succeeded — fold the fields into the record you send:
var lead = Object.assign({ email: emailValue /* …your fields… */ }, mtkFields());
// POST `lead` to your API / CRM / Supabase — attribution rides along.Those dataLayer values reflect page-load resolution. For the exact submit-time conversion timing (mtk_conversion_unix, mtk_conversion_datetime), either use method A (the tag re-resolves at submit) or the tag's own hook below.
Or call window.mtkFormSubmit() — the tag exposes this global on every page. It returns the full field set with submit-accurate timestamps and also pushes the mtk_form_submit dataLayer conversion event. context is optional metadata (e.g. { form_name: "get-started" }) — attribution is browser-keyed, so there's no identifier or email to pass.
// Alternative: the tag's own hook. window.mtkFormSubmit() returns the full
// field set at submit-accurate timestamps AND pushes the mtk_form_submit
// dataLayer event. Pass optional context (e.g. { form_name: '...' }) — there's
// no identifier or email to pass. Call it once, at confirmed success:
var attr = window.mtkFormSubmit({ form_name: "get-started" });
delete attr.event; // drop the event key before storing
var lead = Object.assign({ email: emailValue /* …your fields… */ }, attr);
// POST `lead`. Already fire your OWN confirmed-success conversion event?
// Leave emitOnSubmit off and use mtkFormSubmit only for the return value, so
// you don't double-count conversions.The mtk_form_submit event is a conversion signal (for GTM → GA4 / Meta CAPI / Google) — separate from storing the fields on the lead. If your form already fires its own confirmed-success conversion event, you don't need MTK's: leave Emit form-submit event (emitOnSubmit) off and use mtkFormSubmit()only for its return value, so a single conversion isn't counted twice.
Publish and verify
Publish, then land on the page with a test URL like ?utm_source=googleads&utm_medium=cpc and submit. Confirm the values arrive where submissions are stored — the form entry, your CRM, or the Supabase row — or log mtkFields() in the console to see exactly what resolved.
value; the HTML Name column is the exact key for each dataLayer value (copy button per cell). On WordPress with Contact Form 7, the form template adds rules of its own — see the Contact Form 7 guide. A form inside a cross-origin iframe can't be reached from the page — render the form inline instead. Full field reference at Developer docs; stuck? Troubleshooting.