Developer Guide

Public Pull API v1

A read-only OAuth2 API that lets your CRM, data warehouse, or iPaaS pull case, document, decision, screening, and event data on demand — the perfect alternative or complement to outbound webhooks.

Looking for a CRM or iPaaS recipe instead? See the Integrations Hub for the full list of supported integrations.

1. Overview

The Public Pull API is a read-only HTTPS API mounted at /v1/*. It is designed for enterprise integrations where push delivery (outbound webhooks) isn’t a fit — for example, batch CRM syncs, data-warehouse loaders, or iPaaS recipes that prefer a polling model.

Every response is scoped to the tenant that owns the access token. Cross-tenant requests return 404 uniformly — there is no existence leak.

What you can pull: cases, documents (with extracted data), case decisions, screening hits, and a unified event feed that mirrors our outbound webhook payloads.

2. Authentication

Mint a client_id and client_secret in Compliance Configuration → Public Pull API. The secret is shown once — store it in your CRM/iPaaS secret manager.

Exchange the credentials for a 1-hour Bearer token via the OAuth2 client-credentials grant:

curl -X POST https://www.firstmilelabs.com/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "pk_live_abc123",
    "client_secret": "sk_live_xxxxxxxxxxxx"
  }'

# → { "access_token": "eyJhbGciOi…", "token_type": "Bearer",
#     "expires_in": 3600, "scope": "read:cases read:documents read:events read:decisions read:screening" }

Pass the token as Authorization: Bearer … on every API call. Tokens are signed JWTs; we honour them until expiry, but you can also rotate immediately by revoking the client.

Scopes: read:cases, read:documents, read:events, read:decisions, read:screening, read:full_case (full-parity case egress — only returns data when the tenant has full-parity egress enabled), plus the write scope cases:write (one-click onboarding initiation — see section 7). Subset at creation time if you want to follow least-privilege.

3. Pagination & rate limits

List endpoints return at most limit rows (default 50, max 200) in data, plus an opaque nextCursor and a boolean hasMore. Pass the cursor back verbatim on the next call:

GET /v1/cases?limit=100&cursor=eyJ0Ijoi…

Default rate limit is 120 requests / minute / client (the token endpoint is limited to 30 req/min per client). When throttled we return 429 with a Retry-After header in seconds.

4. Cases

GET /v1/cases returns the case list. Filters: status, updatedSince. GET /v1/cases/{caseId} returns a single case including its current decision and risk band, and /v1/cases/{caseId}/decisions and /v1/cases/{caseId}/screening expose the decision and screening histories.

curl https://www.firstmilelabs.com/v1/cases?limit=50 \
  -H "Authorization: Bearer eyJhbGciOi…"

# → { "data": [ { "caseId": "CS-2026-00471", "companyName": "Acme Ltd",
#                 "status": "approved", "updatedAt": "2026-05-14T09:18:42Z", … } ],
#     "nextCursor": "eyJ0Ijoi…", "hasMore": true }

5. Documents

GET /v1/documents lists every document across the tenant (filter by caseId or status). GET /v1/documents/{id} returns the full record including the structured extractedData JSON produced by AI document intelligence — perfect for hydrating your CRM with the company name, registration number, directors, and so on.

6. Event feed (pull alternative to outbound webhooks)

GET /v1/events mirrors the payloads we send via outbound webhooks, deduped by eventId. Use it when push delivery is impractical (firewalled environments, batch ETL, polling recipes). The shape of data is identical to the corresponding webhook event.

curl "https://www.firstmilelabs.com/v1/events?event_type=case.decision.made&limit=100" \
  -H "Authorization: Bearer eyJhbGciOi…"

# → { "data": [ { "eventId": "evt_…", "eventType": "case.decision.made", "eventVersion": 1,
#                 "occurredAt": "2026-05-14T09:18:42Z", "data": { … } } ],
#     "nextCursor": "eyJ0Ijoi…", "hasMore": false }
#
# The shape of `data` for each `eventType` is defined by the per-event
# JSON Schemas at /schemas/webhooks/<eventType>.v1.json — identical to
# what you receive on push delivery.

For a full payload reference, see the Outbound Webhook Guide.

7. Initiate onboarding (one-click from your CRM)

POST /v1/cases is the one write endpoint on the public surface. A single call creates a case in First Mile Labs, sends the customer their tenant-branded magic-link invite, and starts emitting case.created outbound webhooks back to your CRM. Requires the cases:write scope.

curl -X POST https://www.firstmilelabs.com/v1/cases \
  -H "Authorization: Bearer eyJhbGciOi…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sfdc-0015g00000abc123" \
  -d '{
    "customerEmail": "ceo@acme.example",
    "customerName":  "Jane Doe",
    "companyName":   "Acme Holdings Ltd",
    "country":       "United Kingdom",
    "crmRecordId":   "0015g00000abc123",
    "crmSource":     "salesforce",
    "sendInvite":    true
  }'

# → 201 Created
# {
#   "caseId":            "8a0e8d9c-…",
#   "customerEmail":     "ceo@acme.example",
#   "customerName":      "Jane Doe",
#   "companyName":       "Acme Holdings Ltd",
#   "crmRecordId":       "0015g00000abc123",
#   "crmSource":         "salesforce",
#   "source":            "api",
#   "status":            "in-progress",
#   "inviteSentAt":      "2026-05-15T09:14:11.421Z",
#   "customerPortalUrl": "https://app.firstmilelabs.com/",
#   "analystUrl":        "https://cases.firstmilelabs.com/?case=8a0e8d9c-…"
# }
#
# A second call with the same Idempotency-Key OR the same crmRecordId
# returns the existing case (200 OK, Idempotent-Replay header set) instead
# of minting a duplicate.

Idempotency. Two independent dedup layers, both per-tenant:

  • Idempotency-Key header — caller-supplied. Replays return the original status + body with Idempotent-Replay: true.
  • (orgId, crmRecordId) — pass crmRecordId and a second call for the same upstream record returns the existing case (200 OK, Idempotent-Replay: crm-record-id). Bypass with {"allowDuplicateCrmRecord": true} when you genuinely want a second case.
  • (orgId, customerEmail) open-case guard — a second call for a customer who already has an open (non-closed) case in this tenant returns 409 with { error: 'duplicate_open_case', existing: { caseId, … } }. Bypass with {"allowDuplicate": true} when you genuinely want to mint a parallel case — the override is recorded in the tenant's configuration audit log.

Cases minted this way appear in the analyst dashboard with a coloured API · {CRM} badge so analysts can tell at a glance which rows came in via sales rather than the upload portal.

CRM admins: see the end-to-end Salesforce integration guide (named-credential, Apex helper, Lightning quick-action) or the HubSpot integration guide (Workflow + Custom Code action), or the Zapier integration guide (Webhooks by Zapier — no custom Zapier app required) for the full one-click recipe.

8. Versioning policy

The API namespace is /v1/*. We follow the same additive compatibility policy as our outbound webhooks: new fields may be added at any time, and consumers must ignore unknown fields. Breaking changes ship under a new /v2/* namespace; /v1/* remains supported for the announced sunset window.

9. OpenAPI spec

The full machine-readable spec lives at /v1/openapi.json. Drop it into Postman, Insomnia, or your code-generator of choice to scaffold a typed client in minutes.

10. Connect with Claude (Anthropic Custom Connector)

First Mile Labs ships an OAuth 2.0 authorization-code grant on the same /v1/oauth/* surface, so analysts can connect Claude.ai as a Custom Connector and ask plain-English questions ("which cases are awaiting EDD?", "summarise the screening hits on Acme Holdings") with live data pulled via the Public Pull API.

For the model we run on your behalf inside the platform, what data each AI feature sees, and our encryption / tenant-isolation posture, see the Trust & AI page.

One-time setup (per analyst):

  1. In Claude.ai, go to Settings → Connectors → Add custom connector and paste the FML connector URL: https://www.firstmilelabs.com/v1/oauth/authorize (token endpoint: /v1/oauth/token).
  2. Claude redirects you to firstmilelabs.com. Sign in with your normal analyst account, review the requested scopes on the consent screen, and click Allow.
  3. You're returned to Claude with the connection live. Ask Claude anything about your cases — it will call the Public API on your behalf using a refresh token scoped to your tenant.

Revoking access. Open Compliance Configuration → Public Pull API → Connected Apps and click Disconnect next to Claude. Every access and refresh token Claude holds against your tenant stops working on the next call. The same panel lists every OAuth app any team-mate has connected, so an admin can disconnect on someone's behalf at any time.

Building your own OAuth-based connector? The authorization-code flow is documented in the OpenAPI spec (see /v1/oauth/authorize, /v1/oauth/token with grant_type=authorization_code + refresh_token, and /v1/oauth/revoke). Email partners@firstmilelabs.com to register your client_id and redirect URIs.