Policy Intake

Create an employer and a direct-billing policy in a single call, then immediately create a payment link to collect premium. This is the fast path for partners onboarding a brand-new insured end to end — no pre-provisioning of records in Audit1 required.


Create an Employer & Policy #

POST /api/v1/intake/policies

Authenticate with your X-Client-ID / X-Client-Secret (requires a payments- or all-scoped key). One atomic call creates an employer and a direct billing policy so you can create a payment link for the new policy right away. The carrier is inferred automatically for single-carrier keys; multi-carrier keys must pass carrier_id, and it must be within your key's scope.

Request #

Top-level fields:

Field Type Required Description
idempotency_key string Yes Re-submitting with the same key returns the original ids — never a duplicate
employer object Yes The employer to create or resume (see Employer below)
owners array No Business owners / officers (see Owners below)
policy object Yes The direct-billing policy to create (see Policy below)
carrier_id string Conditional Carrier ObjectId. Required unless your API key is scoped to exactly one carrier — single-carrier keys infer it server-side. Must be within your key's scope

Idempotency & FEIN dedupe

— resubmitting the same idempotency_key returns the original ids. Dedupe also applies on the employer's FEIN within your tenant: resubmitting the same FEIN resumes and returns the same employer instead of creating a duplicate. A FEIN that already exists outside your tenant returns 409 fein_conflict.

Employer

Field Type Required Description
legal_name string Yes Registered legal business name
fein string Yes 9-digit FEIN. An SSN/ITIN is accepted for sole proprietors; normalized to XX-XXXXXXX
entity_type string No Legal structure (e.g., LLC, Corporation)
address object Yes Business address. line1, city, state, and zip are all required
contact_name string No Primary contact name
contact_email string No Primary contact email
phone string No Contact phone
email string No Employer email

Owners

Each entry in owners[] describes an owner or officer. Optional, but recommended for accurate rating.

Field Type Required Description
first_name string Yes Owner first name
last_name string Yes Owner last name
ownership_percentage number No Ownership stake, 0100
excluded boolean No true excludes the officer from coverage. Defaults to false (officer included)
class_code string No NCCI class code for the owner
dob string No Date of birth — recorded for audit purposes
title string No Officer title — recorded for audit purposes

Policy

Field Type Required Description
state string Yes 2-letter state code where coverage applies
class_code string Yes Primary NCCI class code
effective_date string Yes ISO date the policy takes effect. Expiration is set automatically to +1 year
annual_premium_cents integer Yes Annual premium in cents — positive integer ($1,000.00 = 100000)
policy_number string No Your policy number. Auto-assigned if omitted
el_limit_tier string No Employers-liability limit tier — recorded for audit purposes

Response #

201 Created — or 200 OK with the same body on an idempotent replay:

{
  "employer_id": "681xyz789abc123456789012",
  "policy_id": "682def789ghi012345678901"
}

The policy is created in the in_progress state. It flips to active automatically when its payment link completes — at the same moment the payment.completed webhook fires.

Errors #

Status Code Meaning
400 validation error A required field is missing or invalid, or carrier_id is missing on a multi-carrier key
403 out-of-scope carrier The carrier_id is not within your API key's scope
409 fein_conflict The FEIN already exists under a different tenant
502 partial failure Downstream creation partially failed. The response includes an intake_id; retry with the same idempotency_key to resume

Example #

Create the employer and policy:

curl -X POST /api/v1/intake/policies \
  -H "X-Client-ID: $CLIENT_ID" \
  -H "X-Client-Secret: $CLIENT_SECRET" \
  -d '{
    "idempotency_key": "intake-smith-2026-05-01-001",
    "employer": {
      "legal_name": "Smith Industries LLC",
      "fein": "123456789",
      "entity_type": "LLC",
      "address": {
        "line1": "500 Industrial Way",
        "city": "Phoenix",
        "state": "AZ",
        "zip": "85001"
      },
      "contact_name": "Dana Smith",
      "contact_email": "ap@smithindustries.com"
    },
    "owners": [
      {
        "first_name": "Dana",
        "last_name": "Smith",
        "ownership_percentage": 100,
        "excluded": false,
        "class_code": "8810"
      }
    ],
    "policy": {
      "state": "AZ",
      "class_code": "5183",
      "effective_date": "2026-05-01",
      "annual_premium_cents": 100000,
      "policy_number": "WC-2026-0042"
    }
  }'

Response (201 Created):

{
  "employer_id": "681xyz789abc123456789012",
  "policy_id": "682def789ghi012345678901"
}

Then create a pay-in-full link for the returned policy — no carrier_id needed on a single-carrier key:

curl -X POST /api/v1/payment-links \
  -H "X-Client-ID: $CLIENT_ID" \
  -H "X-Client-Secret: $CLIENT_SECRET" \
  -d '{
    "employer_id": "681xyz789abc123456789012",
    "policy_id": "682def789ghi012345678901",
    "payment_type": "ONE_TIME",
    "payment_context": "full_payment",
    "premium_cents": 100000,
    "customer": {
      "name": "Smith Industries LLC",
      "email": "ap@smithindustries.com"
    },
    "after_completion_url": "https://yourplatform.com/thanks"
  }'

The employer_id and policy_id come from the intake response. See Payment Links & Subscriptions for the full payment-link field set and the full_payment payment context.


Sandbox #

Test keys (audit1_test_*) run the identical intake flow with zero Stripe traffic — no real customers, charges, or emails. Use them to build and verify your integration end to end.

Sandbox payment links can be completed programmatically:

POST /api/v1/payment-links/{id}/simulate-completion

Available on sandbox keys only — live keys receive 403. Calling it marks the sandbox payment link as paid and fires the same payment.completed webhook to your sandbox webhook subscriptions, so you can exercise your completion handler without a real payment.

Sandbox vs live

— intake records created with a live key are real production records: a real employer and a real policy. Use audit1_test_* keys for all integration testing.


Webhook: payment.completed #

When the payment link for an intake policy completes, Audit1 fires a payment.completed webhook to every matching subscription (sandbox links fire it to your sandbox subscriptions). The policy flips to active at the same time.

The delivered request body is the event's data object — there is no envelope wrapper. Event metadata travels in the headers:

Header Value
X-Webhook-Signature HMAC-SHA256 signature, hex-encoded
X-Webhook-Timestamp Unix timestamp in milliseconds
X-Webhook-Event payment.completed
X-Webhook-ID Unique event ID

Example body:

{
  "payment_id": "686abc123def456789012345",
  "payment_link_id": "686def456abc789012345678",
  "stripe_payment_link_id": "plink_1PXYZabcdef",
  "policy_id": "682def789ghi012345678901",
  "employer_id": "681xyz789abc123456789012",
  "carrier_id": "680abc456def789012345678",
  "amount_cents": 100000,
  "premium_cents": 100000,
  "fee_cents": 0,
  "payment_type": "ONE_TIME",
  "payment_context": "full_payment",
  "paid_at": "2026-05-01T15:30:00.000Z"
}

amount_cents is the total charged (premium_cents + fee_cents).

Verifying the signature #

Audit1 signs each delivery with the subscription secret returned when you registered the webhook. Recompute and compare:

  1. Read X-Webhook-Signature and X-Webhook-Timestamp from the headers.
  2. Build the signed string ${timestamp}.${raw_body} — the raw request body, exactly as received.
  3. Compute HMAC-SHA256 with your webhook secret and hex-encode it.
  4. Compare against X-Webhook-Signature using a constant-time comparison.

See Payment Webhooks to register an endpoint, view every event type, and get language-specific verification snippets, the retry schedule (1s / 5s / 25s / 125s / 625s), and the auto-disable policy.


All monetary values are in cents. $1,000.00 = 100000. This avoids floating-point precision issues.