Every recipe sends one stage change to POST /api/v1/conversions with a key that has the conversions:write scope (Settings → API keys). Fields are in the API reference. To match conversions to visits, capture the visitor id with hidden form fields (how) and store it on the lead.
Needs: Salesforce Enterprise edition or above (HTTP Callout in Flow), and an admin.
Not yet verified end to end; tell support@geoswap.co if a step differs.
GeoSwap, protocol Custom. Add a principal with a parameter ApiKey = your gs_live_… key, and a custom header Authorization = {!'Bearer ' & $Credential.GeoSwap.ApiKey}.GeoSwap, URL https://geoswap.co, external credential GeoSwap, Generate Authorization Header off, Allow Formulas in HTTP Header on.OccurredAtIso (Text): SUBSTITUTE(TEXT({!$Flow.CurrentDateTime}), " ", "T") — Salesforce writes date-times in GMT with a trailing Z.GeoSwap, method POST, URL path /api/v1/conversions, and paste the sample body below so Salesforce generates the request type. Map the fields as shown.parent_external_id to the Lead Id it was converted from (store it in a custom field such as GeoSwap_Lead_Id__c at conversion). Without one, each stage still counts; it just isn't linked to the lead.""; GeoSwap ignores empty optional fields, so a lead without a visitor id or amount still counts. Keep value in quotes (as below) so a blank Amount stays valid JSON.201 (or 200 on a re-send).{
"event": "qualified",
"external_id": "{!$Record.Id}",
"parent_external_id": "{!$Record.GeoSwap_Lead_Id__c}",
"occurred_at": "{!OccurredAtIso}",
"visitor_id": "{!$Record.GS_Visitor_Id__c}",
"value": "{!$Record.Amount}",
"currency": "USD"
}Needs: HubSpot Data Hub (formerly Operations Hub) Professional or Enterprise for the "Send a webhook" action.
Not yet verified end to end; tell support@geoswap.co if a step differs.
https://geoswap.co/api/v1/conversions.Authorization, value Bearer gs_live_… (the bare key also works), location Request header.event = qualified; external_id = Record ID; occurred_at = Became a Marketing Qualified Lead date (HubSpot sends epoch milliseconds, which GeoSwap accepts); email = Email; visitor_id = the contact property your form saves gs_vid into.hs_object_id) is a number; GeoSwap accepts external_id as either a JSON number or a string, so send it either way.external_id (the Record ID), so GeoSwap links them into one journey automatically.201.Needs: a Zapier plan that includes Webhooks by Zapier (paid plans).
Not yet verified end to end; tell support@geoswap.co if a step differs.
https://geoswap.co/api/v1/conversions, payload type json.event (type the stage name), external_id (the record id), occurred_at (the trigger's date; add a Formatter step to ISO 8601 if it has no time zone), plus email, value, visitor_id if you have them.Authorization = Bearer gs_live_….201 with result: "created".Any language that can make an HTTPS POST. Batch up to 500 per request with /api/v1/conversions/batch.
GEOSWAP_API_KEY); never ship it to a browser.gs_vid cookie or hidden field, and tags for things like site and market.429 (after Retry-After) and 5xx; re-sending the same event + external_id is safe.// Node 18+ (fetch built in). Send each stage when it happens.
const res = await fetch("https://geoswap.co/api/v1/conversions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GEOSWAP_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"event": "qualified",
"external_id": "006Hs00001AbCdE",
"parent_external_id": "00QHs00002XyZqR",
"occurred_at": "2026-10-12T14:03:00Z",
"visitor_id": "Xb3k9_Qm2LpA7vR1.20261003",
"email": "jane@example.com",
"value": 1200,
"currency": "USD",
"tags": {
"market": "co"
}
}),
});
if (res.status === 429) {
// Rate limited: wait Retry-After seconds, then retry.
} else if (!res.ok) {
const { error } = await res.json(); // { code, message, request_id }
console.error(error.code, error.request_id);
}