Zapier integration guide

One-click KYB Onboarding from Zapier

Build a Zap that fires off a First Mile Labs case the moment your CRM, form, or spreadsheet says "this lead is ready for KYB". Two Webhooks by Zapier steps — one to mint an OAuth2 token, one to call POST /v1/cases with an Idempotency-Key — and your customer gets their tenant-branded magic-link invite without anyone touching the analyst console.

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_ID and FML_CLIENT_SECRET once, 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          : yes

Tokens 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          : yes

A 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-Key header — caller-supplied. Replays return the original status + body verbatim, with Idempotent-Replay: true. We use zapier-{{1__record_id}} in the snippet so Zap retries are safe.
  • (orgId, crmRecordId) dedup — pass crmRecordId: "{{1__record_id}}" and a second call for the same upstream record returns the existing case (200 OK with Idempotent-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 have cases:write; mint a new one or extend the existing client's scopes in Compliance Configuration → Public Pull API.
  • 400 validation_errorcustomerEmail is 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 same Idempotency-Key was 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.