# Intake insurance claims with an email agent

> Source: <https://dev.to/mqasimca/intake-insurance-claims-with-an-email-agent-478f>
> Published: 2026-07-04 13:07:29+00:00

Most "AI for insurance" demos point a model at a claims handler's mailbox and call it triage. That's fine until you realize the agent is squatting in a human's inbox, racing the human for unread mail, and inheriting every OAuth-token and shared-mailbox-permission headache you were trying to escape. First-notice-of-loss (FNOL) intake is document-heavy and routing-sensitive — a single claim email shows up with photos of a dented bumper, a PDF police report, and a half-filled claim form, and all of it has to land in front of the *right* adjuster, in one place, with nothing dropped. That's not a job for a bot reading over someone's shoulder. It's a job for an inbox that *is* the agent.

That's what a Nylas **Agent Account** gives you: a real, sendable, receivable mailbox — `claims@yourcarrier.com`

— that your code owns end to end. No human shares it. No OAuth refresh dance. Under the hood it's *just a grant* with a `grant_id`

, so every endpoint you already know (Messages, Threads, Folders, Attachments) works against it unchanged. The agent receives the FNOL, downloads the evidence, files a claim folder, acknowledges the claimant, and hands the whole thing to an adjuster.

I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for when I'm wiring this up. I'll show every step two ways — the raw `curl`

and the `nylas`

equivalent — so you can drop either into your stack. One honest caveat up front: this post is about **intake capture and routing**, not claims adjudication. Deciding whether a claim pays out is your business logic and your model. Getting the claim, its documents, and a clean handoff to a human adjuster — that's what the platform does for you.

`message.created`

webhook; outbound acknowledgements go through the same Messages API as any grant.`CLM-2026-0481`

has one home instead of being scattered across a shared inbox.If you've ever run claims intake off a shared Google or Microsoft mailbox, you know the failure modes: two automations both marking the same mail read, token expiry at 2 a.m., and no clean way to give your code its own identity. The Agent Account flips that. The mailbox *is* the integration. The mental model is the spine of everything below: **nothing new to learn on the data plane.** If you've built against a connected grant before, this is the same `/v3/grants/{grant_id}/*`

surface — same auth header, same payloads — except you provisioned the grant yourself in one API call and there's no end user to re-authenticate.

You'll need a Nylas API key and a registered sending domain (a custom domain, or a Nylas `*.nylas.email`

trial subdomain to start). New domains warm over roughly four weeks, so stand the mailbox up early. The base host in these examples is `https://api.us.nylas.com`

, and every call authenticates with `Authorization: Bearer <NYLAS_API_KEY>`

.

New to Agent Accounts? Skim the

[Agent Accounts overview]and the[supported endpoints reference]first — this post assumes you know what a grant is.

Create the account with a `POST /v3/connect/custom`

. Provider is `nylas`

, the email lives on your registered domain, and an optional top-level `name`

sets the display name claimants see.

```
curl --request POST \
  --url "https://api.us.nylas.com/v3/connect/custom" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "provider": "nylas",
    "name": "Claims Intake",
    "settings": { "email": "claims@yourcarrier.com" }
  }'
```

The response carries a `grant_id`

— hold onto it, it's the only handle you need for everything that follows. The same thing from the CLI:

```
nylas agent account create claims@yourcarrier.com --name "Claims Intake"
```

No `--workspace`

flag here — the API auto-creates a default workspace and policy on account creation. If you want stricter limits or spam tuning later (say, a tighter attachment cap on a prototype agent), you attach a custom policy to the workspace with `nylas workspace update <workspace-id> --policy-id <policy-id>`

. For FNOL you'll often *raise* the inbound attachment limits rather than lower them — claimants send big photo bundles.

Inbound mail fires the standard `message.created`

webhook. Webhooks on Nylas are **application-scoped**, not grant-scoped: you subscribe once at the app level with `POST /v3/webhooks`

, and events for every grant in the app arrive at that one endpoint, each payload carrying the `grant_id`

you filter on.

```
curl --request POST \
  --url "https://api.us.nylas.com/v3/webhooks" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "trigger_types": ["message.created"],
    "webhook_url": "https://claims.yourcarrier.com/hooks/nylas",
    "description": "FNOL intake"
  }'
```

Or from the CLI — same subscription, one command:

```
nylas webhook create \
  --url https://claims.yourcarrier.com/hooks/nylas \
  --triggers message.created \
  --description "FNOL intake"
```

Two things to get right in the handler, because claims intake cannot afford to act on a claim twice:

**Dedup on the notification id.** Nylas guarantees at-least-once delivery and will retry the same event up to three times. The top-level notification

`id`

is constant across all retries of one event — that's your delivery dedup key. The inner `data.object.id`

is the **Don't trust the payload for the body.** Treat the `message.created`

payload as a pointer, not the document. Fetch the full message by id when you need the body, and branch on `message.created.truncated`

— when a message exceeds ~1 MB the trigger name changes and the body is omitted, so you re-fetch regardless. FNOL emails with inline photos cross that line constantly.

And verify the signature before you do any of it. Nylas signs each webhook with `X-Nylas-Signature`

— a hex HMAC-SHA256 of the **raw** request body using your webhook secret. Compare it with a constant-time compare, but guard that both buffers are equal length first (`crypto.timingSafeEqual`

throws on a length mismatch). Locally, the CLI does it for you:

```
nylas webhook verify \
  --payload-file ./raw-body.json \
  --signature "<X-Nylas-Signature header value>" \
  --secret "<your-webhook-secret>"
```

Once the webhook hands you a `message_id`

, pull the whole thing — body, headers, and the attachment metadata you'll need next.

```
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/<MESSAGE_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"
```

From the CLI, `nylas email read`

does the same and prints it cleanly:

```
nylas email read <message-id> <grant-id>
```

This is also where your FNOL extraction happens. The claimant's name, the policy number, the loss date, the description of what happened — that's a model call against the body you just fetched, and the resulting **claim state** lives in your own database. Agent Accounts don't support custom `metadata`

on messages, so don't try to stash the claim number on the message itself — own that mapping in your app. One note on reading: fetching with GET does *not* mark the message read. Marking read is a separate `PUT /v3/grants/{id}/messages/{id}`

with `{"unread": false}`

if you want it.

This is the part of FNOL that makes or breaks the workflow. The message you fetched lists its attachments under an `attachments`

array, and each entry's `id`

is the value you pass everywhere a download or metadata call asks for an attachment id. Watch this one: the endpoint path placeholder is `{attachment_id}`

, but the value you put there comes from the attachment object's `id`

field (not a separate `attachment_id`

key). Pull the metadata first if you want to check the filename and content type before committing to a download:

```
nylas email attachments show <attachment-id> <message-id> <grant-id>
```

Then stream the bytes to disk. Over the API, the download endpoint **requires** the `message_id`

query parameter:

```
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/attachments/<ATTACHMENT_ID>/download?message_id=<MESSAGE_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --output ./claims/CLM-2026-0481/bumper-photo.jpg
```

The CLI takes the attachment id and message id positionally and writes the file for you with `-o`

:

```
nylas email attachments download <attachment-id> <message-id> <grant-id> \
  -o ./claims/CLM-2026-0481/bumper-photo.jpg
```

Loop over the attachment list and you've captured the whole evidence bundle — photos, the PDF claim form, the police report — into the claim's working directory. From here it's yours: store it, run it through document AI, attach it to the claim record. The platform's job was to get it out of an email and into your hands intact.

Keeping every open claim in one inbox is how mail gets lost. Create a folder per claim so the FNOL message and its thread have a single home an adjuster can open.

```
curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/folders" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{ "name": "CLM-2026-0481" }'
nylas email folders create "CLM-2026-0481" <grant-id>
```

Both return the new folder's `id`

. System folder names (`inbox`

, `sent`

, `drafts`

, `trash`

, `junk`

, `archive`

) are reserved, so name folders by claim number or adjuster queue.

Moving the message into the claim folder is a `PUT`

that sets the message's `folders`

array. Add the claim folder, drop `inbox`

, and the FNOL is filed.

```
curl --request PUT \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/<MESSAGE_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{ "folders": ["<CLAIM_FOLDER_ID>"] }'
nylas email move <message-id> --folder <claim-folder-id> <grant-id>
```

Now, *which* adjuster? You have two levers, and it's important to be precise about which is which.

**Sender-based routing is a Rule.** If a partner body shop or a fraud-flagged domain always goes to the same queue, an inbound Rule handles it server-side, before your app even sees the mail. Inbound rules match on `from.*`

only — `from.address`

, `from.domain`

, `from.tld`

— with operators `is`

, `is_not`

, `contains`

, and `in_list`

, and can `assign_to_folder`

, `block`

, `mark_as_read`

, and so on. So "auto-file everything from `@trusted-bodyshop.com`

into the priority queue" is a clean Rule. Remember a Rule is inert until you attach it to a workspace via `rule_ids`

(`PATCH /v3/workspaces/{id}`

, or `nylas workspace update --rules-ids`

).

**Claim-type classification is not a Rule.** Here's the line developers trip over: an inbound Rule cannot read the subject or the body. It only sees the sender. So "route auto claims to the auto desk and property claims to the property desk based on what the email

`PUT messages/{id}`

/ `nylas email move`

you just saw. The Rule engine routes by Close the loop the moment intake succeeds — claimants who get an instant "we've got your claim, here's your number" call back far less. Reply in-thread so the acknowledgement groups with the original FNOL.

```
curl --request POST \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "reply_to_message_id": "<MESSAGE_ID>",
    "to": [{ "email": "claimant@example.com" }],
    "body": "Thanks — your claim is logged as CLM-2026-0481. An adjuster will reach out within one business day."
  }'
```

The `to`

field is required on the send endpoint even for a reply — populate it with the original sender's address (you have it from the message you fetched). The `reply_to_message_id`

is what preserves the thread (Nylas sets the `In-Reply-To`

and `References`

headers for you). From the CLI, `nylas email reply`

fetches the original to populate recipient and subject, and threads automatically:

```
nylas email reply <message-id> <grant-id> \
  --body "Thanks — your claim is logged as CLM-2026-0481. An adjuster will reach out within one business day."
```

A few things I'd flag before you ship this:

`DELETE /v3/grants/{id}/messages/{id}`

moves mail to Trash, not into the void — pass `?hard_delete=true`

for a permanent wipe. The CLI's `nylas email delete`

only trashes. For a regulated workflow where a claimant invokes erasure, the only true full wipe is deleting the grant (`DELETE /v3/grants/{id}`

/ `nylas agent account delete`

). Don't tell compliance a plain delete is "permanent" — it isn't.`id`

before you act, not after.`rule-evaluations`

audit trail for "why did this claim get filed there?"`nylas agent overview`

to see your accounts, policies, and rules at a glance.Intake is the unglamorous half of claims that decides whether the glamorous half ever works. Get the email, the documents, and the handoff right, and your adjusters open a clean claim instead of digging through a shared inbox. The Agent Account gives you a mailbox that does exactly that — and it's the same grant you already know how to drive.

When this post is published, link AI agents and crawlers to the retrieval-ready version on `cli.nylas.com`

:
