Coverly Developer Docs

Everything you need to integrate Coverly into your agency, platform, or carrier workflow. Build white-label quote experiences, submit applications programmatically, and receive real-time events via webhooks.

What you can build

Quote Widget Embed
Drop a copy-paste snippet into any page. Covers the full intake flow — no API calls required on your end.
Application API
POST applications from your existing quoting system. Coverly handles underwriting and policy issuance.
Webhook Events
Subscribe to lead.created, quote.generated, application.submitted, application.underwritten, policy.issued, and contact.tier_changed in real time.
Partner Attribution
Track every lead back to your broker or MGA with partner_id routing and per-partner notification settings.

Base URLs

# Sandbox — safe to test, no real data
https://sandbox.coverly-8.polsia.app

# Production
https://coverly-8.polsia.app

Rate limits

Default soft limit is 100 requests/minute per API key. If you need a higher limit for a high-volume integration, contact us at partners@coverly.app.

Quote Widget Embed

Add the Coverly quote widget to any page with a single script tag. Fully customizable via query params. No iframe, no API calls needed from your side.

Basic snippet

Copy and paste into your HTML <body>
<!-- Coverly Quote Widget -->
<script>
  window.coverlyConfig = {
    partner_id: 'your_partner_id',   // Your numeric partner ID (e.g. 42)
    theme: 'dark'                  // 'dark' | 'light'
  };
</script>
<script src="https://coverly-8.polsia.app/embed/coverly.js"></script>

Configuration options

ParamTypeDescription
partner_idstringYour partner identifier. Routes leads to your CRM and sets notification preferences.
themestringdark (default) or light. Matches the widget's container background.
container_idstringDOM element ID to mount the widget into. Defaults to creating a floating bubble.

postMessage events

The widget fires postMessage events to the parent window so your page can react to quote lifecycle events.

window.addEventListener('message', (event) => {
  const data = JSON.parse(event.data);
  switch (data.type) {
    case 'quote.started':
      console.log('Lead started quote', data.payload);
      break;
    case 'quote.completed':
      console.log('Quote delivered', data.payload);
      break;
    case 'apply.submitted':
      console.log('Application submitted', data.payload);
      break;
  }
});

Event payloads

// quote.started
{
  "type": "quote.started",
  "payload": {
    "leadId": "cl_01J8X...",    // CRM contact ID
    "partnerId": "acme_broker",
    "timestamp": "2026-07-04T10:32:00Z"
  }
}

// quote.completed
{
  "type": "quote.completed",
  "payload": {
    "leadId": "cl_01J8X...",
    "quoteId": "qv_01J9A...",
    "carriers": ["AIG", "Lincoln", "Northwestern", "Pacific Life"],
    "tier": "hot",             // 'hot' | 'warm' | 'cold'
    "aiScore": 92
  }
}

// apply.submitted
{
  "type": "apply.submitted",
  "payload": {
    "leadId": "cl_01J8X...",
    "applicationId": "app_01JB2...",
    "carrier": "AIG"
  }
}
Need a standalone embed? Use /embed/quote?partner_id=xxx&theme=dark as a full-page redirect. Works without any script tag.

Single-script embed (with auto-bind attribution)

No iframe, no webhook code required. Drop /embed.js on any page with a Bearer API key on the script src — the form posts to POST /api/v1/leads and the contact is attributed to your partner account under the same spine as /quote/start and direct API calls.

<!-- Coverly Quote Widget (single-script bundle) -->
<script src="https://coverly-8.polsia.app/embed.js?partner=ck_live_<your_key>&primary_color=%23e86b2c&theme=dark"></script>
<div data-coverly-quote></div>

If you omit data-coverly-quote, the bundle mounts a floating action button (data-coverly-fab) at the bottom-right of the host page. Lead attribution flows through the partner=ck_live_… query string — the bundle adds that key as Authorization: Bearer ck_live_… on the POST. Loaded cross-origin from partner sites — the bundle calls /api/v1/leads directly with the same auth your API key already covers.

Configuration options

ParamTypeDescription
partnerstringRequired. Your ck_live_… API key. Used as Authorization: Bearer on POST /api/v1/leads.
primary_colorstringOptional. Default #e86b2c. Hex used for accents + submit button.
themestringOptional. dark (default) or light.
Snippet generator: visit /for-brokers/embed?partner_id=… to copy a rendered snippet (with your live key + brand color baked in). The page also shows live embedding status — embedded_at, first_lead_at, and a 30-day lead count.

Application API

Submit an insurance application programmatically. Coverly routes it through the carrier's underwriting engine, issues a policy number, and fires a webhook to your endpoint.

POST /api/apply

Request headers

HeaderValue
Content-Typeapplication/json
AuthorizationBearer ck_live_<your_key>

Request body

FieldTypeRequiredDescription
quoteIdstringrequiredCoverly quote ID returned from the widget event quote.completed.
carrierstringrequiredCarrier name selected by the lead. One of: AIG, Lincoln, Northwestern Mutual, Pacific Life.
monthlyPricenumberOverride monthly premium (optional). If omitted, uses the price from the quote response.
cURL
curl -X POST https://coverly-8.polsia.app/api/apply \\
  -H "Content-Type: application/json" \\
  -H "Authorization: Bearer ck_live_a3f9xxxxxxxxxxxxxxxx" \\
  -d '{
    "quoteId": "qv_01J9A3K...",
    "carrier": "AIG",
    "monthlyPrice": 187
  }'
Node.js / fetch
const res = await fetch('https://coverly-8.polsia.app/api/apply', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ck_live_a3f9xxxxxxxxxxxxxxxx'
  },
  body: JSON.stringify({
    quoteId: 'qv_01J9A3K...',
    carrier: 'AIG',
    monthlyPrice: 187
  })
});
const data = await res.json();
console.log(data);

Response

{
  "ok": true,
  "applicationId": "app_01JB2...",
  "underwriting": {
    "decision": "approved",       // 'approved' | 'declined' | 'review'
    "score": 87,
    "riskBand": "standard",      // 'preferred' | 'standard' | 'substandard'
    "reason": "Clean MVR, stable income",
    "policyNumber": "CVLY-2026-00942",
    "policyIssued": true,
    "documentType": "pdf"
  }
}
Policy issuance is automatic for approved applications. Declined or review cases do not receive a policy number. Your webhook listener receives the final outcome.

Quote API — Health

Generate marketplace health-insurance quotes programmatically. Coverly accepts household + income + coverage preferences, runs the 2-method APTC calculation against 2026 HHS poverty guidelines, and returns a tiered plan grid (Bronze/Silver/Gold/Platinum) across 5 representative carriers.

POST /api/quote/health

Request body

FieldTypeRequiredDescription
zipCodestringrequired5-digit US ZIP code (e.g. 90210).
householdSizeintegerrequiredNumber of members seeking coverage.
memberAgesstringrequiredComma-separated ages of covered members (e.g. "42,38,12").
householdAginumberrequiredAnnual household Adjusted Gross Income in USD.
tobaccobooleanAny covered member uses tobacco. Default false.
employerCoverageAvailablebooleanWhether employer-sponsored coverage is available. Default false.
currentlyUninsuredbooleanDefault false.
planTierPreferencestringbronze|silver|gold|platinum|any. Default any.
hsaEligiblebooleanRestrict results to HSA-eligible (HDHP) plans.
deductibleBandstringMax deductible in dollars ("500"|"1500"|"3500"|"6500") or "any".
mustHaveRxstringFreeform Rx / provider requirements (stored on the lead, not on individual plans).
namestringLead name. Optional — leads can be anonymous.
emailstringLead email. Optional but required to enroll the lead in the nurture sequence.
cURL
curl -X POST https://coverly-8.polsia.app/api/quote/health \\
  -H "Content-Type: application/json" \\
  -d '{
    "zipCode": "90210",
    "householdSize": 3,
    "memberAges": "42,38,12",
    "householdAgi": 85000,
    "planTierPreference": "silver",
    "name": "Jane Doe",
    "email": "jane@example.com"
  }'

Response

{
  "ok": true,
  "quote": {
    "productLine": "health",
    "aptcMonthly": 350,
    "aptcAnnual": 4200,
    "aiTier": "hot",
    "aiScore": 88,
    "plans": [
      {
        "carrier": "Blue Cross Blue Shield",
        "metal": "silver",
        "monthlyPremiumPre": 485,
        "monthlyPremiumPost": 135,
        "deductible": 3500,
        "moop": 7500,
        "network": "Broad PPO",
        "hsaEligible": false,
        "isBestValue": true
      }
    ]
  }
}
APTC math: the engine compares the FPL cap method (8.5% of income, all incomes ≥ 100% FPL) against the sliding-scale method (2.04–8.5% of income, ≤ 250% FPL) and picks the smaller required contribution — resulting in the larger subsidy for the consumer. Households above 400% FPL are ineligible for APTC.

Webhook Events

Configure webhooks at /for-brokers/webhooks?partner_id=<id>. Every delivery is signed with HMAC-SHA256; failed deliveries retry on an exponential backoff for up to 24 hours.

Events

lead.created
Fired when a new lead is captured (embed or partner-attributed). Payload includes contactId, name, email, zipCode, vehicle, aiTier, aiScore, source.
quote.generated
Fired when carrier quotes are ready. Payload includes contactId, quoteId, carriers[], monthlyPriceRange, aiTier.
application.submitted
Fired when an application is POSTed to /api/apply. Payload includes applicationId, contactId, carrier, quoteId.
application.underwritten
Fired after the underwriting decision is recorded. Payload includes applicationId, decision, score, riskBand, reason.
policy.issued
Fired when underwriting approves and a policy number is generated. Payload includes applicationId, contactId, carrier, policyNumber, monthlyPrice.
contact.tier_changed
Fired when an existing contact moves between hot / warm / cold. Payload includes contactId, fromTier, toTier.

Webhook payload

{
  "id": "evt_5f8d3a90-...",
  "type": "policy.issued",
  "created": 1752248712,
  "data": {
    "applicationId": "42",
    "contactId": "318",
    "carrier": "AIG",
    "policyNumber": "CVLY-2026-00942",
    "monthlyPrice": 187
  }
}

Request headers

HeaderFormat
Content-Typeapplication/json
Coverly-EventThe event type, e.g. policy.issued.
Coverly-Signaturet=<unix-seconds>,v1=<hex-hmac-sha256>. HMAC input is ${t}.${rawBody} using your endpoint's signing secret.
User-AgentCoverly-Webhooks/1.0

Verifying signatures

Every webhook is signed with HMAC-SHA256 over ${timestamp}.${rawBody} using your endpoint's secret. Use a timing-safe comparison and reject requests whose timestamp is more than 5 minutes off — this is the security-critical bit Stripe-style signers miss.

HMAC verification (Node.js)
const crypto = require('crypto');
const express = require('express');

function verifyCoverlySignature(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map(p => p.split('='))
  );
  const t   = parseInt(parts.t, 10);
  const v1  = parts.v1;

  // 1. Reject stale or far-future timestamps (replay-attack defense).
  const skew = Math.abs(Math.floor(Date.now() / 1000) - t);
  if (!Number.isFinite(t) || skew > 300) return false;

  // 2. Recompute HMAC over `${timestamp}.${rawBody}` and timing-safe compare.
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(v1, 'hex');
  const b = Buffer.from(expected, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post('/webhooks/coverly',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['coverly-signature'] || '';
    if (!verifyCoverlySignature(req.body, sig, process.env.COVERLY_WEBHOOK_SECRET)) {
      return res.status(401).send('Invalid signature');
    }
    const event = JSON.parse(req.body.toString('utf8'));
    // event.id is stable across retries — use it for idempotency.
    // event.type is one of: lead.created, quote.generated, application.submitted, ...
    // event.data is event-type-specific (see above).
    res.status(200).send('ok');
  });
HMAC verification (Python)
import hmac, hashlib, time

def verify_coverly_signature(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split('=', 1) for p in header.split(','))
    try:
        t = int(parts['t'])
        v1 = parts['v1']
    except (KeyError, ValueError):
        return False

    # 1. Reject timestamps more than 5 minutes off.
    if abs(int(time.time()) - t) > 300:
        return False

    # 2. Recompute HMAC and compare.
    signed = f"{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(v1, expected)

# In Flask:
from flask import request, abort

@app.route("/webhooks/coverly", methods=["POST"])
def webhook():
    sig = request.headers.get("Coverly-Signature", "")
    if not verify_coverly_signature(request.get_data(), sig, WEBHOOK_SECRET):
        abort(401)
    event = request.get_json()
    # event["id"] is stable across retries — use it for idempotency.
    return "ok", 200
Signing secret is set per endpoint in the Webhooks dashboard. Rotate it any time from /for-brokers/webhooks — your endpoint keeps receiving deliveries during rotation, but deliveries will fail to verify until you update your server with the new secret.
Open Webhooks Dashboard → Register endpoint URLs, pick events, view recent deliveries, replay failures, and rotate signing secrets.

Public API

The structured integration surface — push leads, query policies, validate webhooks.

// POST a new lead
curl -X POST https://coverly-8.polsia.app/api/v1/leads \\
  -H "Authorization: Bearer ck_live_..." \\
  -H "Content-Type: application/json" \\
  -d '{ "email": "...", "zipCode": "...", "vehicle": {...} }'
Open Public API Reference → Every endpoint documented with cURL, headers (incl. rate-limit), errors, and ownership scoping. Per-key limits: 60 rpm / 1000 rph.

Authentication

All API requests require a partner API key. Keys are prefixed by environment:

ck_live_<32-char-key>   // Production key
ck_test_<32-char-key>   // Sandbox key
For the structured partner surface (leads, quotes, policies, webhooks/test, …), see the Public API Reference — same Bearer auth + per-key rate limit + audit logging.

Request signing (recommended)

For additional security, sign each request body with HMAC-SHA256 and include the signature in the X-Coverly-Signature header.

const crypto = require('crypto');

function signRequest(body, secret) {
  const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
  const sig = crypto.createHmac('sha256', secret).update(bodyStr).digest('hex');
  return sig;
}

Getting your API key

Generate and manage your API keys via the self-serve portal. Each key is shown once at creation — store it in your password manager or secrets manager.

Open API Key Management → Enter your numeric partner ID to access your keys. For webhook endpoints and signing secrets, see Webhooks.

Key management

Each key is prefixed ck_live_ (production) or ck_test_ (sandbox). Only the first 12 characters of the key body are visible in the portal — the full key is shown once on generation and cannot be recovered. Revoke old keys immediately after rotation.

# Example key prefix — shown in the portal
ck_live_a3f9...

# Full key (shown once, never stored)
ck_live_a3f9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Partner Dashboard Authentication

Partners access the dashboard at /for-brokers/dashboard using a magic-link flow — no passwords required.

Magic-link flow

POST /api/partner-auth/request

Request a magic link by email. If the account exists, an email is sent immediately; otherwise the endpoint returns success anyway to prevent email enumeration.

FieldTypeRequiredDescription
emailstringrequiredThe partner email address on file.
Request
{
  "email": "you@agency.com"
}
Response
{
  "ok": true,
  "message": "Magic link sent"
}

The email contains a time-limited link. Clicking it triggers the verification endpoint.

GET /api/partner-auth/verify?token=<token>

Validates the token, sets the partner_session cookie, and redirects to /for-brokers/dashboard. Token expires after 15 minutes.

Invalid or expired tokens redirect to /for-brokers/login?error=invalid. Unknown email redirects to /for-brokers/login?error=not_found.

Session cookie

PropertyValue
Namepartner_session
TypeJWT (HS256)
Expiry7 days
FlagshttpOnly, sameSite: lax, secure in production

Logout

POST /for-brokers/logout

Clears the partner_session cookie and redirects to /for-brokers/login.

Error codes

CodeMeaning
invalidToken was tampered with, malformed, or expired. Request a new magic link.
not_foundNo partner account exists for that email address.

Environments

EnvironmentBase URLDescription
● Sandbox https://sandbox.coverly-8.polsia.app Isolated test environment. No real carriers, no real data. Perfect for integration testing.
● Production https://coverly-8.polsia.app Live environment. Real carrier APIs, real data, real webhooks.

Sandbox keys start with ck_test_. Production keys start with ck_live_. Keys from one environment do not work in the other.

Changelog

2026-07-17

Public v1 API + Self-Serve Key Rotation

Six endpoints under /api/v1: POST /leads, GET /leads, GET /leads/:id, POST /quotes, GET /policies/:id, POST /webhooks/test. Bearer ck_live_... auth, per-key rate limit (60 rpm / 1000 rph) with X-RateLimit-* headers, api_request_log audit row per call. Self-serve rotate at POST /api/partner-keys/:id/rotate — new plaintext once; old key blocked at the verification edge immediately. Environment label (live | test) on every key. Full reference.

2026-07-11

Health Insurance Intake + Marketplace Quote Engine

3-step health intake at /quote/health (household basics → income/subsidy → coverage preferences). Marketplace-style plan grid (Bronze/Silver/Gold/Platinum × 5 representative carriers: BCBS, UHC, Ambetter, Oscar, Kaiser). 2-method APTC calculation against 2026 HHS poverty guidelines (8.5% FPL cap vs. 100–250% sliding scale — picks the smaller required contribution). CRM partition via contacts.product_line; partner dashboard shows Life vs. Health leads on a new product-mix tile.

2026-07-11

Webhook Delivery Infrastructure

Six lifecycle events (lead.created, quote.generated, application.submitted, application.underwritten, policy.issued, contact.tier_changed) POST to partner URLs with HMAC-SHA256 signing (Coverly-Signature: t=<ts>,v1=<hex>). Exponential-backoff retry (1m, 5m, 30m, 2h, 6h, 24h), max 6 attempts; auto-disable after 20 consecutive failures. Self-serve endpoint management + delivery log + replay at /for-brokers/webhooks.

2026-07-04

Self-Serve API Key Management + Partner Attribution

Partner API keys now self-serve at /for-brokers/api-keys?partner_id=<id>. Keys prefixed ck_live_ (32-byte random, sha256 hashed — plaintext never stored). One-time display of full key on generation, copy-to-clipboard UI, revoke flow. Optional partner attribution on POST /api/apply via Authorization: Bearer ck_live_... header — routes leads to partner dashboard. Rate limited to 5 key operations/hour per partner. Docs updated.

2026-07-04

Embed Widget Launch

White-label embed widget at /embed/quote. Floating/inline script loader with postMessage height resize. Snippet generator at /for-brokers/embed with syntax-highlighted copy button. Partner notification routing via partner_id.

2026-07-02

Competitive Positioning Pages

Four competitor comparison pages (/vs/superagent, /vs/pathwork, /vs/zowie, /vs/kinro) with side-by-side feature matrices, Coverly win arguments, and CTAs.

2026-06-21

Underwriting Engine + Application Flow

Full application lifecycle: POST /api/apply, automated underwriting decision (approved/declined/review), policy number issuance, and policy.issued webhook. AI-powered decision scoring with risk band classification.

2026-07-25

Lincoln Financial Live Connector

Lincoln Financial — life (term/whole/IUL) + auto connector promoted from stub to live-ready. Env-gated on LINCOLN_API_KEY / LINCOLN_API_URL (and optional LINCOLN_LIFE_API_URL). Falls back to sim when unconfigured.

2026-06-19

Carrier API Integration Layer

Pluggable carrier connector architecture (lib/carrier-api/), AIG proof-of-concept connector, stubs for Lincoln, Northwestern Mutual, and Pacific Life. Live wins over simulation, simulation fills gaps until live API keys are configured.