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.
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.
Create a cancellation
POST /cancellations accepts JSON. Unknown fields are allowed and retained verbatim. consumer.phone or consumer.email is required.
{
"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.
{
"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.
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.
{
"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:
{
"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.
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.
Read & withdraw
/cancellations/:idGets one partner-owned cancellation.
{
"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.
/cancellations?stage=collecting_info&page=1Returns { data, page, page_size, total }; page size is 50.
/cancellations/:id/withdrawMoves 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.
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.
Signed event delivery
Events, delivered as the case moves. Deliveries retry five times with exponential backoff.
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:
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.