> ## Documentation Index
> Fetch the complete documentation index at: https://docs.turrisfi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent License Expiring

> Webhook event triggered 60 days before a producer license reaches its renewal date

## Overview

The `AGENT_LICENSE_EXPIRING` webhook is triggered when one of your producers' licenses is **60 days** from its renewal date.

It exists because a lapsing license did not reliably reach you. [Agent Compliance Status Change](/guides/webhooks/agent-compliance-status-change) fires per product and state, and only when a compliance status actually flips, so a license expiring in a state you have no requirement for, or one covered by another license the producer holds, is silent. Nothing announced the approaching date itself.

**Webhook type:** `AGENT_LICENSE_EXPIRING`

### Triggers

One delivery per license, on the day it is exactly 60 days from its `nextRenewalDate`.

<Info>
  Deliveries land on a daily sweep at **05:00 America/New\_York**, not in real time. This is a date crossing rather
  than a change to a record, so there is nothing to react to the moment it happens.
</Info>

<Warning>
  **The 60 days here is not the 30 days in the Turris web app.** The app's own "Expiring Soon" badge appears at 30
  days, so between 60 and 30 days out you will hold a license this webhook called expiring that the screen does not.

  Both numbers are correct for their purpose and neither is changing. That is why every delivery carries
  `daysUntilExpiration` **and** `expiringWindowDays`: read them rather than inferring a threshold, and the two
  surfaces reconcile.
</Warning>

## What does not fire this event

| Change                                      | Use instead                                                                                                                                                           |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The license passes its renewal date         | [Agent License Expired](/guides/webhooks/agent-license-expired)                                                                                                       |
| A license with **no** `nextRenewalDate`     | Nothing. Perpetual and non-expiring licenses never fire either event                                                                                                  |
| A license whose `status` is `inactive`      | Nothing. NIPR has already flagged it                                                                                                                                  |
| A license revoked or suspended for cause    | [Agent Regulatory Action Added](/guides/webhooks/agent-regulatory-action-added), or [Agent Compliance Status Change](/guides/webhooks/agent-compliance-status-change) |
| An agency license reaching its renewal date | [Agency License Expiring](/guides/webhooks/downstream-entity-license-expiring)                                                                                        |
| Appointments, or product/state compliance   | [Agent Compliance Status Change](/guides/webhooks/agent-compliance-status-change)                                                                                     |

### Licenses the producer chose not to renew DO fire

If a producer has marked a license do-not-renew, this event **still fires**. That is deliberate and probably the opposite of what you would guess: Turris suppresses its own renewal reminders in that case, because there is no point nagging someone about a license they decided to drop.

For a carrier the reasoning inverts. A producer deliberately letting a license lapse is one of the most actionable things you can hear, so you get told.

### Perpetual licenses never fire, and there are more than you might think

Both events are driven entirely by a range query on `nextRenewalDate`. A license without one cannot match, so it never produces a delivery.

That is **roughly 9% of producer licenses** on the platform. If you reconcile deliveries against your own license count you will find a gap, and this is it, not dropped events. Read [Get Agent Licenses](/api-reference/v2/licenses/agent-licenses) for the full picture.

### There is no "expired" license status to key on

Worth stating plainly, because it looks like a bug in the payload: `status` is copied verbatim from NIPR and nothing in Turris derives it from a date. So a license 60 days from expiry, and one that lapsed last week, both normally carry `status: "active"`.

Use `daysUntilExpiration` and `nextRenewalDate`. Do not use `status` to infer expiry.

## Payload Example

```json theme={null} theme={null}
{
  "webhookType": "AGENT_LICENSE_EXPIRING",
  "upstreamEntityId": "507f1f77bcf86cd799439010",
  "payload": {
    "licenseId": "507f1f77bcf86cd799439020",
    "niprDataSubscriptionId": "507f1f77bcf86cd799439014",
    "npn": "1234567",
    "stateCode": "CA",
    "licenseNumber": "0K12345",
    "licenseClassCode": "PC",
    "licenseClassName": "Property & Casualty",
    "status": "active",
    "residencyStatus": "Resident",
    "issueDate": "2019-04-01T00:00:00.000Z",
    "nextRenewalDate": "2026-10-19T00:00:00.000Z",
    "linesOfAuthority": [
      { "code": "PROP", "name": "Property", "status": "Active" },
      { "code": "CAS", "name": "Casualty", "status": "Active" }
    ],
    "daysUntilExpiration": 60,
    "expiringWindowDays": 60,
    "agentId": "507f1f77bcf86cd799439015",
    "downstreamEntityAssociationId": "507f1f77bcf86cd799439011",
    "legalName": "RT Specialty LLC",
    "branchName": "RT Specialty - Miami Beach, FL"
  }
}
```

### Payload Fields

| Field                           | Type                  | Description                                                                                      |
| ------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------ |
| `licenseId`                     | string                | Turris's id for this license record.                                                             |
| `niprDataSubscriptionId`        | string                | The NIPR data subscription this license was retrieved under.                                     |
| `npn`                           | string                | The producer's National Producer Number.                                                         |
| `stateCode`                     | string                | State that issued the license, e.g. `CA`.                                                        |
| `licenseNumber`                 | string                | The license number as filed with the state.                                                      |
| `licenseClassCode`              | string                | License class code, e.g. `PC`.                                                                   |
| `licenseClassName`              | string                | Human-readable class, e.g. `Property & Casualty`.                                                |
| `status`                        | string                | Verbatim NIPR status, `active` or `inactive`. **Never reflects expiry** — see above.             |
| `residencyStatus`               | string                | Resident or non-resident.                                                                        |
| `issueDate`                     | string \| null        | ISO-8601. When the license was issued.                                                           |
| `nextRenewalDate`               | string                | ISO-8601. Always present: a license without one cannot fire this event.                          |
| `linesOfAuthority`              | array                 | `code`, `name` and `status` per line of authority.                                               |
| `daysUntilExpiration`           | number                | Whole days from the sweep date to `nextRenewalDate`. Positive on this event.                     |
| `expiringWindowDays`            | number                | The threshold that triggered the delivery, currently `60`. Read this rather than hard-coding it. |
| `agentId`                       | string, may be absent | The producer. Use it to call [Get Agent Licenses](/api-reference/v2/licenses/agent-licenses).    |
| `downstreamEntityAssociationId` | string, may be absent | The agency relationship the producer writes under. The identifier every other webhook carries.   |
| `legalName`                     | string, may be absent | Legal name of the **agency**, not of the producer.                                               |
| `branchName`                    | string, may be absent | Branch name of that relationship.                                                                |

<Note>
  **The entity fields are best-effort, and they all depend on one lookup.** `agentId`,
  `downstreamEntityAssociationId`, `branchName` and `legalName` are resolved from your association with the
  producer's agency. If that lookup misses — an agency archived between our read and the delivery is the realistic
  case — **none of them arrive**. If it hits, `downstreamEntityAssociationId` is always present and the rest are
  individually best-effort.

  Absent means "we could not resolve this", never a statement about the data. Re-read from the API rather than
  inferring anything from an absent field.

  **One caveat.** When a producer works under more than one of your agencies, the agency fields name an arbitrary one
  of those relationships, because a single NIPR subscription is shared across them. `agentId` is exact; treat
  `downstreamEntityAssociationId` as one of possibly several. Match on `agentId`.
</Note>

<Note>
  There is deliberately **no `expirationDate`** field. `nextRenewalDate` is the only expiry date the platform holds.
  (The license GET reference currently documents an `expirationDate`; that field does not exist on any record and no
  response has ever carried a value for it.)
</Note>

## Deduplication and volume

Each license matches the 60-day boundary on exactly **one** calendar day, so you receive exactly one delivery per license per renewal cycle. There is no debouncing and no batching. A renewal that moves `nextRenewalDate` forward re-arms the event for the new date rather than cancelling anything already sent.

<Warning>
  **Size your receiver for the spike, not for the median.** The two are three orders of magnitude apart. A typical
  sweep delivers **single digits**, and the 95th percentile is still under **50** — but the busiest measured sweep for
  a single carrier carries about **2,800 deliveries of this event alone**.

  Return `2xx` quickly and queue the work rather than processing inline. A receiver that handles the median sweep
  comfortably can still fall over on a peak one.
</Warning>

<Warning>
  If the daily sweep does not run, that day's licenses are **not** announced later. There is no catch-up pass. If you
  need a guarantee rather than a notification, reconcile against
  [Get Agent Licenses](/api-reference/v2/licenses/agent-licenses) on a schedule and treat this webhook as the fast path.
</Warning>

<Warning>
  Webhook subscriptions are per event type. Subscribing to Agent License Expiring does **not** deliver
  [Agent License Expired](/guides/webhooks/agent-license-expired) — register a separate webhook for each.
</Warning>


## Related topics

- [Agency License Expiring](/guides/webhooks/downstream-entity-license-expiring.md)
- [Agent License Expired](/guides/webhooks/agent-license-expired.md)
- [Changelog](/changelog.md)
