API reference

PolicyCancel Partner API

POST a cancellation with whatever you have, track it by case id, and receive HMAC-signed webhooks as the case moves from intake to a verified outcome.

Getting started

Base URL & authentication

Base URL: https://policycancel.com/api/v1 (use your deployment URL in sandbox).

Send every request with Authorization: Bearer pc_sand_… or pc_live_…. Keys are shown only once when created and may be revoked by a partner admin.

Cancellations

Create a cancellation

POST /cancellations accepts JSON. Unknown fields are allowed and retained verbatim. consumer.phone or consumer.email is required.

POST /api/v1/cancellations
{
  "external_ref": "ref-4711",
  "consumer": {
    "name": "Jordan Rivera",
    "phone": "+15125550123",
    "email": "jordan@example.com",
    "address": { "line1": "1 Main St", "city": "Austin", "state": "TX", "zip": "78701" },
    "dob": "1990-04-02",
    "timezone": "America/Chicago"
  },
  "new_policy": { "carrier": "Clearcover", "policy_number": "CC-991", "effective_date": "2026-07-15" },
  "prior_policies": [{ "carrier": "State Farm", "policy_number": "SF-123" }],
  "requested_cancel_date": "2026-07-15",
  "consent": { "contact_attested": true },
  "metadata": {}
}

A new case returns HTTP 201. A create that replays an Idempotency-Key returns the same body with HTTP 200 — branch on 2xx, not on 201 alone.

201 Created
{
  "id": "uuid",
  "stage": "collecting_info",
  "case_url": "https://policycancel.com/dashboard/cases/uuid",
  "tracker_url": "https://policycancel.com/c/token",
  "sandbox": true,
  "consumer_outreach": "partner",
  "consumer_message": "Hi Jordan, congrats on your new policy! Use this link to cancel your prior policy with State Farm: https://policycancel.com/c/token You'll confirm a few details and sign; the cancellation request is then submitted for you and your old carrier confirms with you directly.",
  "prior_policies": [
    { "id": "policy-uuid", "carrier": "State Farm", "policy_number": "SF-123", "us_state": "TX", "status": "pending" }
  ]
}

Store id as the PolicyCancel case ID in your CRM. case_url opens the partner dashboard for an authorized organization member, while tracker_urlis the consumer's private progress and action link. It does not expire; the consumer confirms their date of birth or ZIP code the first time they open it.

consumer_outreach says who contacts the consumer: policycancel (default: we text and email the link and keep the consumer updated) or partner (your agents send the link; PolicyCancel sends no SMS or voice outreach for your cases, while the intro email still carries the link and later consumer updates fall back to email). consumer_messageis a ready-to-send message containing the link, rendered with the consumer's first name and the prior carriers, with no brand headline: paste it into your own text or chat thread. A workspace admin switches the mode under Settings → Consumer outreach.

Validation errors return 422 with error and actionable issues. Malformed JSON returns 400. Authentication failures return 401 with a reason (missing_header, malformed_header, unknown_or_revoked, mode_mismatch) and a hint; keys are mode-specific, so create a new key after a workspace goes live.

Multi-policy

Several policies on one cancellation

Send every policy the consumer is cancelling in prior_policies, in the order you want them shown. The consumer gets one link, confirms each policy, and signs once; each carrier receives only its own signed ACORD 35.

POST /api/v1/cancellations
{
  "consumer": { "name": "Jordan Rivera", "email": "jordan@example.com", "address": { "state": "TX" } },
  "new_policy": { "carrier": "Clearcover", "effective_date": "2026-07-15" },
  "prior_policies": [
    { "carrier": "Progressive", "policy_number": "PG-1" },
    { "carrier": "Geico", "policy_number": "GE-2", "us_state": "TX" }
  ],
  "requested_cancel_date": "2026-07-15"
}

The response names every carrier in consumer_message and lists each policy with its own status:

201 Created (excerpt)
{
  "consumer_message": "Hi Jordan, congrats on your new policy! Use this link to cancel your prior policies with Progressive and Geico: https://policycancel.com/c/token You'll confirm a few details and sign; the cancellation request is then submitted for you and your old carrier confirms with you directly.",
  "prior_policies": [
    { "id": "policy-uuid-1", "carrier": "Progressive", "policy_number": "PG-1", "us_state": "TX", "status": "pending" },
    { "id": "policy-uuid-2", "carrier": "Geico", "policy_number": "GE-2", "us_state": "TX", "status": "pending" }
  ]
}

Each policy is submitted to its carrier separately, so prior_policies[].status can differ between policies while stage describes the case as a whole:

pending
Nothing sent yet: the consumer has not signed, or the case is waiting on something (a document, an agent contact) before it can be sent.
awaiting_release
Assembled and waiting in your release queue.
submitting
Being sent to the carrier.
submitted
Delivered to the carrier (or sent by our operations team).
verified
The carrier (or the consumer, with evidence) confirmed this policy is cancelled.
self_serve
The carrier requires the consumer to finish in its portal; the tracker walks them through it.
failed
Delivery failed and operations is following up.
manual_processing
Our operations team sends this one by hand (Massachusetts policies, with Form 2-A). The case is not marked complete until it has gone.
canceled
Never sent: the case was withdrawn or closed before this carrier request went out.

A multi-policy case reaches completed_verified only when every policy is verified (or was sent by our operations team). If a carrier has not confirmed within 45 days of its own submission, that policy times out; the case becomes completed_unverified once no policy is still waiting, so one slow carrier never closes the case while another is still pending.

Idempotency

Retry safely

Add Idempotency-Key: <unique value> to safely retry a create. A replay returns the same case with HTTP 200; stage, consumer_outreach and consumer_message reflect the case as it is now. Reusing the key with a different JSON body returns 409, so the same request never double-opens a case.

Lifecycle

Read & withdraw

GET/cancellations/:id

Gets one partner-owned cancellation.

200 OK
{
  "id": "uuid",
  "stage": "completed_verified",
  "case_url": "https://policycancel.com/dashboard/cases/uuid",
  "tracker_url": "https://policycancel.com/c/token",
  "sandbox": true,
  "consumer_outreach": "partner",
  "consumer_message": "Hi Jordan, congrats on your new policy! …",
  "external_ref": "ref-4711",
  "requested_cancel_date": "2026-07-15",
  "new_policy": { "carrier": "Clearcover", "policy_number": "CC-991", "effective_date": "2026-07-15" },
  "created_at": "2026-07-14T18:30:00.000Z",
  "prior_policies": [
    { "id": "policy-uuid", "carrier": "State Farm", "policy_number": "SF-123", "us_state": "TX", "status": "submitted" }
  ],
  "documents": [
    { "kind": "signed_form", "url": "https://policycancel.com/api/artifacts/artifact-uuid", "bytes": 48231, "created_at": "2026-07-15T20:10:00.000Z" }
  ]
}

documents is always an array and includes available signed_form, signature_certificate, and dec_page artifacts. Each URL downloads through the partner-authorized artifact route. On a multi-policy case the signed_form is the consumer's combined copy of every signed form; carriers only ever receive their own. prior_policiescarries each policy's current status, as in the create response.

GET/cancellations?stage=collecting_info&page=1

Returns { data, page, page_size, total }; page size is 50.

POST/cancellations/:id/withdraw

Moves a non-terminal case to withdrawn and cancels pending timers, messages and any carrier submission that has not been sent yet (including one waiting in your release queue). Invalid transitions return 409.

Bulk

CSV intake

Use data/fixtures/intake-template.csv. The fixed header flattens the JSON fields. Import returns created cases plus row-numbered errors; valid rows are committed even when another row fails. external_ref supplies CSV retry idempotency.

Webhooks

Signed event delivery

Events, delivered as the case moves. Deliveries retry five times with exponential backoff.

case.stage_changedcase.needs_partner_actioncase.submittedcase.verifiedcase.unable_to_complete

The JSON envelope is { id, type, created_at, data }. The signature header is X-PolicyCancel-Signature: t=<unix>,v1=<hex>. Verify the raw request bytes before parsing:

verify.ts
const [tPart, v1Part] = signature.split(",");
const expected = hmacSha256(secret, `${tPart.slice(2)}.${rawBody}`);
timingSafeEqual(v1Part.slice(3), expected);

Reject timestamps outside your tolerance (five minutes is recommended) and compare digests in constant time. Return any 2xx status to acknowledge delivery.