1. Overview
The recipe has three pieces: an OAuth2 client credential minted in Compliance Configuration → Public Pull API (with the cases:write scope), any Zap trigger that signals "time to start KYB" (new HubSpot deal, new Salesforce opportunity, new Typeform submission, new row in a Google Sheet, …), and two Webhooks by Zapier POST actions — the first mints a short-lived bearer token, the second calls POST /v1/cases with an Idempotency-Key. Outbound webhooks then push status changes back to the same upstream record via the crmRecordIdyou sent on the create call.
Why no custom Zapier app: the Public API is plain HTTPS + OAuth2 client-credentials, so the off-the-shelf Webhooks by Zapier built-in covers it cleanly. No app review, no Zapier partner agreement, no extra surface to maintain.
2. Client credentials
In First Mile Labs go to Compliance Configuration → Public Pull API → New clientand tick the cases:write scope. Copy the client_id and client_secret — the secret is shown once.
In Zapier, store both values somewhere safe. Two common patterns:
- Zapier Storage by Zapier — set
FML_CLIENT_IDandFML_CLIENT_SECRETonce, then reference them as{{storage.FML_CLIENT_ID}}from any Zap. - Per-Zap secret fields — paste the values directly into the token step's Data fields. Quick to set up; the trade-off is they're duplicated across Zaps.
3. Pick a Zap trigger
Any trigger that exposes the company name, country, and a primary contact email will do. Common picks:
- HubSpot — "Deal stage updated" or "Company property updated" (e.g. Ready for KYB).
- Salesforce — "New opportunity in stage" or "Updated record" on the Account object.
- Pipedrive — "Updated deal" matching a custom Ready for KYB filter.
- Typeform / Tally / Jotform — "New submission" on your prospect intake form.
- Google Sheets / Airtable — "New or updated row" when an analyst flips a checkbox column.
Whichever trigger you pick, make sure its sample payload includes a stable upstream record id (HubSpot Company ID, Salesforce Account ID, Airtable record id, sheet row id, …). That id powers both the Idempotency-Key and the crmRecordId round-trip in the next two steps.
4. Token step (Webhooks by Zapier · POST)
Add a Webhooks by Zapier action, choose the POST event, then paste these values. Once it runs Zapier will expose {{2__access_token}} for use in step 5.
// Step 2 — Webhooks by Zapier · POST
// Mints a short-lived OAuth2 bearer token. The response's access_token
// is referenced from the next step via {{2__access_token}}.
URL : https://www.firstmilelabs.com/v1/oauth/token
Payload Type : json
Data:
grant_type : client_credentials
client_id : {{storage.FML_CLIENT_ID}} // or hard-coded
client_secret : {{storage.FML_CLIENT_SECRET}} // use Zapier Storage
// or a Zap-level secret
Headers:
Content-Type : application/json
Unflatten : yesTokens are short-lived (one hour). Because each Zap run mints a fresh one, you never need to refresh-rotate manually.
5. Create-case step (Webhooks by Zapier · POST)
Add a second Webhooks by Zapier · POST action and map the trigger's fields into the body. The Idempotency-Key header is what makes the action safe to retry — Zapier will replay this step on transient errors and we'll return the original response verbatim.
// Step 3 — Webhooks by Zapier · POST
// Creates (or replays) the FML case. The Idempotency-Key uses the
// upstream record id so workflow retries and double-fires never
// create duplicates.
URL : https://www.firstmilelabs.com/v1/cases
Payload Type : json
Data:
customerEmail : {{1__primary_contact_email}}
customerName : {{1__primary_contact_name}}
companyName : {{1__company_name}}
country : {{1__company_country}}
crmRecordId : {{1__record_id}}
crmSource : hubspot // or "salesforce", "pipedrive",
// "intercom", "airtable", …
sendInvite : true
Headers:
Authorization : Bearer {{2__access_token}}
Content-Type : application/json
Idempotency-Key : zapier-{{1__record_id}}
Unflatten : yesA successful first call returns 201 Created with the new case id and the customer portal URL — the customer also receives their tenant-branded magic-link invite automatically because sendInvite: true:
HTTP/1.1 201 Created
{
"caseId": "8a0e8d9c-7b1a-4f2e-9c3b-2d1e8f7a4b6c",
"customerEmail": "[email protected]",
"customerName": "Jane Doe",
"companyName": "Acme Holdings Ltd",
"country": "United Kingdom",
"entityType": null,
"crmRecordId": "rec_8d2…",
"crmSource": "hubspot",
"source": "api",
"status": "in-progress",
"inviteSentAt": "2026-05-15T09:14:11.421Z",
"customerPortalUrl": "https://app.firstmilelabs.com/",
"analystUrl": "https://cases.firstmilelabs.com/?case=8a0e8d9c-…"
}Map caseId back onto the upstream record (a third Zap step — "Update Company" in HubSpot, "Update Record" in Airtable, …) into a single-line text field such as fml_case_id. That gives analysts and ops a deep link straight to the FML case from any CRM row.
6. Webhook round-trip
Every outbound webhook event for this case (case.created, case.submitted, case.decision.made, …) carries the crmRecordId you supplied on the create call. Point your outbound webhook URL at a second Zap whose trigger is Webhooks by Zapier · Catch Hook, then map crmRecordId onto the right CRM update action. See the Webhook Integration Guide for the payload schemas and signature-verification details.
Source badge in the analyst console: cases minted via this flow show an orange API · {crmSource} badge in the dashboard so analysts immediately know the case originated from sales tooling, not the upload portal.
7. Idempotency — never mint a duplicate case
The endpoint is dedup-safe in two independent ways, and both layers are tenant-scoped:
Idempotency-Keyheader — caller-supplied. Replays return the original status + body verbatim, withIdempotent-Replay: true. We usezapier-{{1__record_id}}in the snippet so Zap retries are safe.(orgId, crmRecordId)dedup — passcrmRecordId: "{{1__record_id}}"and a second call for the same upstream record returns the existing case (200 OKwithIdempotent-Replay: crm-record-id) instead of creating a duplicate. This protects you when a Zap is paused-and-resumed, when the trigger re-fires on a re-saved record, or when two Zaps point at the same source.
HTTP/1.1 200 OK
Idempotent-Replay: crm-record-id
{
"caseId": "8a0e8d9c-…", // existing case for this CRM record
"crmRecordId": "rec_8d2…",
"source": "api",
...
}8. Troubleshooting
401 invalid_client— the client secret in Zapier Storage (or the Data field) is stale; re-paste from FML.403 insufficient_scope— the client doesn't havecases:write; mint a new one or extend the existing client's scopes in Compliance Configuration → Public Pull API.400 validation_error—customerEmailis missing or not a valid email; the response body lists each failed field. Add a Zapier Filter step before the create-case action to skip rows without a contact email.409 idempotency_key_conflict— the sameIdempotency-Keywas previously used on a different endpoint; vary the prefix (e.g.zapier-cases-…).- Token step returns an empty
access_token— double-check Payload Type: json and that Unflatten is on; without those, Webhooks by Zapier sends form-encoded bodies which the token endpoint rejects.
For the full request/response contract see the Public API guide or the machine-readable OpenAPI spec.