# The grant_id: One Handle for Mail, Calendar, and Webhooks

> Source: <https://dev.to/qasim157/the-grantid-one-handle-for-mail-calendar-and-webhooks-4k1b>
> Published: 2026-06-15 18:56:27+00:00

Anyone who's wired an autonomous agent into email and calendar the traditional way knows the identifier sprawl: an OAuth client ID, a refresh token per user, a Gmail-specific message ID format, a Microsoft Graph calendar ID, and a webhook subscription ID for each — all with different lifetimes, all able to break independently. Half your "integration" code is really identifier bookkeeping.

Nylas [Agent Accounts](https://developer.nylas.com/docs/v3/agent-accounts/) collapse all of that into one value. When you create an account (the feature's in beta), the response hands you a `grant_id`

, and that single string is the handle for everything the agent does — mail, calendar, contacts, attachments, and the webhooks reporting on all of them.

Every operation addresses the same path family, `/v3/grants/{grant_id}/*`

:

```
POST   /v3/grants/{grant_id}/messages/send         # send mail
GET    /v3/grants/{grant_id}/messages              # read the inbox
GET    /v3/grants/{grant_id}/threads/{thread_id}   # full conversation
GET    /v3/grants/{grant_id}/attachments/{id}/download
POST   /v3/grants/{grant_id}/events                # host a meeting
POST   /v3/grants/{grant_id}/events/{id}/send-rsvp # respond to one
GET    /v3/grants/{grant_id}/contacts
GET    /v3/grants/{grant_id}/rule-evaluations      # audit trail
```

This isn't an abstraction invented for agents — an Agent Account is literally just another grant, the same primitive used for connected Gmail and Outlook accounts. The [supported endpoints reference](https://developer.nylas.com/docs/v3/agent-accounts/supported-endpoints/) puts it as "same endpoints, same auth, same payloads." Anything you built for connected accounts works against an agent's grant unchanged.

And the resources behind the ID are real: six system folders provisioned automatically (`inbox`

, `sent`

, `drafts`

, `trash`

, `junk`

, `archive`

), a primary calendar that speaks standard iCalendar, and outbound messages capped at 40 MB total.

The inbound side completes the picture. You subscribe once at the application level, and every notification — `message.created`

, `event.updated`

, `message.bounce_detected`

, and the rest — carries the `grant_id`

of the account it happened to:

```
{
  "type": "message.created",
  "data": {
    "object": {
      "object": "message",
      "id": "<MESSAGE_ID>",
      "grant_id": "<NYLAS_GRANT_ID>",
      "subject": "Hello from Nylas",
      "from": [{ "email": "sender@example.com", "name": "Sender" }],
      "snippet": "This is a sample message"
    }
  }
}
```

So the dispatch logic in your webhook handler is one lookup: `grant_id`

→ which agent → which handler. One detail to plan for: when a message body exceeds ~1 MB, the trigger arrives as `message.created.truncated`

with the body omitted — you fetch the full message through, naturally, the same `grant_id`

.

The handle appears at birth and disappears at death, with nothing extra to manage in between. Creation is a single call — `POST /v3/connect/custom`

with `"provider": "nylas"`

— and the response's `data.id`

*is* the `grant_id`

:

```
{
  "request_id": "5967ca40-a2d8-4ee0-a0e0-6f18ace39a90",
  "data": {
    "id": "b1c2d3e4-5678-4abc-9def-0123456789ab",
    "provider": "nylas",
    "grant_status": "valid",
    "email": "sales-agent@agents.yourcompany.com",
    "scope": [],
    "created_at": 1742932766
  }
}
```

No refresh token in that payload, because there's no OAuth provider behind the grant. Reconfiguration is also one call against the same ID — `PATCH /v3/grants/{grant_id}`

with a new `workspace_id`

moves the account under a different policy and rule set. And teardown is deleting the grant, which emits `grant.deleted`

through the same webhook subscription that reported its mail. Provision, operate, govern, destroy: four phases, one identifier.

Since one ID anchors everything, the schema almost writes itself. The agents table needs surprisingly few columns:

```
CREATE TABLE agents (
  id            UUID PRIMARY KEY,
  grant_id      TEXT NOT NULL UNIQUE,  -- the Nylas handle
  email         TEXT NOT NULL,
  workspace_id  TEXT,                  -- policy/rule inheritance
  purpose       TEXT,                  -- 'support', 'outreach', ...
  created_at    TIMESTAMPTZ
);
```

A few practices that fall out of the docs:

`provider`

field alongside the grant.`provider: "nylas"`

— that's the documented way to branch between "a human's mailbox we're acting on" and "a mailbox we own."`grant_id`

.`grant.created`

, `grant.updated`

, `grant.deleted`

, and `grant.expired`

tell you about fleet changes. The docs note agent grants rarely expire, because there's no OAuth token behind them to refresh — one whole failure class gone.Worth knowing where the boundary sits. Policies, rules, and lists — the guardrail resources — are application-scoped, with no grant ID in the path. They reach the agent indirectly: a workspace carries a `policy_id`

and `rule_ids`

, the grant carries a `workspace_id`

, and inheritance does the rest. So the grant is the handle for everything the agent *does*, while workspaces govern what it's *allowed* to do. The exception that proves the rule: `GET /v3/grants/{grant_id}/rule-evaluations`

brings the audit trail back under the grant, answering "which rules ran on this account's mail?"

The plan limits split along the same lines, and it's useful to know which scope each one attaches to. The send quota — 200 messages per account per day on the free plan, no daily cap by default on paid plans — is per account. Storage (3 GB on the free plan) is per *organization*, shared across every agent. Retention (30 days inbox, 7 days spam on the free plan) comes through the workspace's policy. Three limits, three different scopes, and only one of them keyed by the grant — which is exactly the kind of thing your data model should encode rather than discover in production.

One more thing that hangs off the grant rather than the application: protocol access. Set an `app_password`

on the grant and the same mailbox opens in Outlook, Apple Mail, or Thunderbird over IMAP and SMTP — useful when a human needs to supervise what the agent has been doing, and another case where the grant is the unit that carries the capability.

The single-handle design sounds like a small ergonomic win, but it compounds. Provisioning returns one value to persist; teardown deletes one grant; debugging starts from one ID that appears in every request path and every webhook payload.

Try this as a next step: take whatever per-user integration table you have today, count the identifier columns, and see how many survive a port to this model. If the answer is "one plus a workspace reference," your migration is mostly a column-drop. The [overview](https://developer.nylas.com/docs/v3/agent-accounts/) is the right place to start reading.
