DEV Community

Qasim
Qasim

Posted on

Warm a sending domain when you provision a new agent tenant

Most "give every tenant its own AI agent" walkthroughs end at the moment the grant exists. You register a domain, create the agent account, fire off a "welcome, I'm your assistant" email, and call the onboarding done. That's fine right up until the first batch of those emails lands in spam — because a brand-new tenant domain has zero sending reputation, and a cold blast of mail from an unknown domain is exactly the shape mailbox providers are trained to filter.

I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for when I'm scripting tenant provisioning. The interesting part of this post isn't deliverability in the abstract — there's already a good general post on that. The angle here is narrower and more operational: treating domain warm-up as a step in your provisioning pipeline. When you onboard a new tenant and spin up its agent on a fresh domain, the warm-up ramp isn't an afterthought you run later — it's part of standing the tenant up. So this is a two-angle tour (curl + CLI) through the whole sequence: register and verify the domain, provision the agent, enforce a daily send cap that matches the ramp, send the first warm-up batch, and watch the bounce/complaint signals so you can back off if reputation dips.

What you're actually provisioning

An Agent Account is just a grant. That's the whole spine of this post. Once it exists, it has a grant_id and works with every grant-scoped endpoint you already know — Messages, Drafts, Threads, Folders, Contacts, Calendars, Events, Webhooks. There's nothing new to learn on the data plane. You send mail with POST /v3/grants/{grant_id}/messages/send, the same call you'd use for a connected Gmail grant.

The difference is on the reputation plane. A connected Google or Microsoft grant inherits the provider's established sending reputation. An Agent Account sends from a domain you own, through Nylas-managed infrastructure, so its inbox placement is yours to build from nothing. That's the catch the demos skip: provisioning the grant is instant, but earning the right to send at volume is a four-week process.

So the provisioning checklist for a new tenant on a new domain looks like this:

  1. Register and verify the domain (DKIM, SPF, ownership, MX).
  2. Provision the tenant's Agent Account on that domain.
  3. Attach a policy that caps daily sends to this week's ramp number.
  4. Send a small warm-up batch to engaged recipients.
  5. Subscribe to deliverability webhooks and watch for bounces/complaints.

Steps 1–4 you do once at onboarding. Step 3 you revisit weekly as the ramp climbs. Let's walk it.

Step 1: Register and verify the tenant's domain

Authentication is the foundation. Nothing else matters if recipient servers can't confirm the mail is really from you. For Agent Accounts that comes down to records you publish during domain setup: DKIM adds a cryptographic signature proving the message wasn't altered and came from your domain, and SPF authorizes Nylas to send on your behalf. Both are verified before a custom domain can host accounts — so by the time an Agent Account exists, its domain is already DKIM- and SPF-authenticated. Nylas also tracks DMARC and ARC, but it doesn't enforce them, which means DMARC is the one layer you add yourself (more on that in the guardrails section).

Honest note on the tooling here: there is no nylas domain command. I checked — the CLI doesn't manage domains. Domain registration runs through the Nylas Dashboard at dashboard-v3.nylas.com/organization/domains, or through the Manage Domains API. And that API isn't a plain Authorization: Bearer call — it uses Nylas Service Account authentication, which signs each request with four custom headers. So the curl below is real, but you'll be computing a signature, not pasting an API key:

# Register the tenant's domain (Manage Domains API — Service Account auth)
curl -X POST "https://api.us.nylas.com/v3/admin/domains" \
  -H "Content-Type: application/json" \
  -H "X-Nylas-Signature: <BASE64_SIGNATURE>" \
  -H "X-Nylas-Kid: <SERVICE_ACCOUNT_ID>" \
  -H "X-Nylas-Nonce: <NONCE>" \
  -H "X-Nylas-Timestamp: 1742932766" \
  -d '{
    "name": "Acme tenant 4821 mail",
    "domain_address": "mail.tenant4821.example.com"
  }'
Enter fullscreen mode Exit fullscreen mode

Then you fetch the DNS records to publish and trigger verification:

# Get the records to publish for a verification type (ownership, dkim, spf, feedback, mx)
curl -X POST "https://api.us.nylas.com/v3/admin/domains/<DOMAIN_ID>/info" \
  -H "Content-Type: application/json" \
  -H "X-Nylas-Signature: <BASE64_SIGNATURE>" \
  -H "X-Nylas-Kid: <SERVICE_ACCOUNT_ID>" \
  -H "X-Nylas-Nonce: <NONCE>" \
  -H "X-Nylas-Timestamp: 1742932766" \
  -d '{ "type": "dkim" }'

# After you add the records at your DNS provider, trigger the check for that type
curl -X POST "https://api.us.nylas.com/v3/admin/domains/<DOMAIN_ID>/verify" \
  -H "Content-Type: application/json" \
  -H "X-Nylas-Signature: <BASE64_SIGNATURE>" \
  -H "X-Nylas-Kid: <SERVICE_ACCOUNT_ID>" \
  -H "X-Nylas-Nonce: <NONCE>" \
  -H "X-Nylas-Timestamp: 1742932766" \
  -d '{ "type": "dkim" }'
Enter fullscreen mode Exit fullscreen mode

Both /info and /verify take a required type in the body — one of ownership, mx, spf, dkim, or feedback — so you run the pair once per record type. For Agent Accounts you verify all five (ownership, dkim, spf, feedback, and mx); that mx record is the extra one Agent Accounts need beyond what Transactional Send requires. There is no CLI for this flow — domain registration and verification are Dashboard-or-API only, so the curl above (Service Account auth) is the programmatic path; I'm not inventing a nylas domain command because none exists. Configure the DNS records before the first /verify call to dodge DNS-caching delays. If verification fails with correct records, give it up to 24 hours (usually it's minutes, depending on TTL).

If you'd rather skip DNS entirely while you build the pipeline, you can provision the agent on a free *.nylas.email trial subdomain — but a real per-tenant domain is the point of this exercise, so I'll assume a custom one.

Step 2: Provision the tenant's Agent Account

With the domain verified, the grant is one call. Create it with POST /v3/connect/custom, provider: "nylas", and settings.email on the verified domain. No refresh token, no OAuth round-trip:

# Provision the tenant's agent — API
curl -X POST "https://api.us.nylas.com/v3/connect/custom" \
  -H "Authorization: Bearer <NYLAS_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "nylas",
    "name": "Acme Support Agent",
    "settings": {
      "email": "support@mail.tenant4821.example.com"
    }
  }'
Enter fullscreen mode Exit fullscreen mode

The optional top-level name sets the agent's display name. The response carries the grant_id — that's your handle for everything downstream.

The CLI collapses this to one command. It always uses provider=nylas, auto-creates the Nylas connector the first time if it doesn't exist, and the API auto-creates a default workspace and policy for the account:

# Provision the tenant's agent — CLI
nylas agent account create support@mail.tenant4821.example.com \
  --name "Acme Support Agent"
Enter fullscreen mode Exit fullscreen mode

I verified both --name (display name, 1–256 chars) and --app-password (an optional IMAP/SMTP app password) on nylas agent account create. There is deliberately no --workspace flag on create — the workspace and its policy come into existence automatically, and you attach a custom policy afterward. Which is exactly what step 3 is for.

Step 3: Cap the daily ramp with a policy

Here's the division of labor that matters: your application decides the warm-up schedule and which recipients to send to. Nylas enforces the ceiling. The ramp logic — how many to send today, spread across the day, to whom — lives in your code. But you can make Nylas backstop your ramp with a hard daily send cap, so a bug in your scheduler can't accidentally blast 1,000 emails on day one.

That cap is a policy field: limit_count_daily_email_sent. Create a policy with this week's number, then attach it to the agent's workspace.

# Create a policy that caps daily sends to week 1's number — API
curl -X POST "https://api.us.nylas.com/v3/policies" \
  -H "Authorization: Bearer <NYLAS_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Warm-up week 1 — tenant 4821",
    "limits": {
      "limit_count_daily_email_sent": 15
    }
  }'
Enter fullscreen mode Exit fullscreen mode

The CLI mirrors it. nylas agent policy create takes --name for a simple policy, or --data / --data-file for a full request body — and the limits live under a limits object, so you pass the JSON through --data:

# Create the capped policy — CLI
nylas agent policy create \
  --data '{"name":"Warm-up week 1 — tenant 4821","limits":{"limit_count_daily_email_sent":15}}'
Enter fullscreen mode Exit fullscreen mode

Then attach the policy to the workspace the account was created in. Grab the workspace ID from nylas workspace list. On the API side this is a PATCH /v3/workspaces/{workspace_id} with the policy_id in the body:

# Attach the policy to the agent's workspace — API
curl --request PATCH \
  --url "https://api.us.nylas.com/v3/workspaces/<WORKSPACE_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "policy_id": "<POLICY_ID>"
  }'
Enter fullscreen mode Exit fullscreen mode

The CLI is the same operation in one line:

# Attach the policy to the agent's workspace — CLI
nylas workspace update <WORKSPACE_ID> --policy-id <POLICY_ID>
Enter fullscreen mode Exit fullscreen mode

I checked nylas workspace update: --policy-id attaches a policy, and passing --policy-id "" detaches it — which maps to setting policy_id to null in the API body (plan maximums apply when nothing's attached). At the end of each warm-up week you bump the policy's limit_count_daily_email_sent to the next tier and the ceiling rises with your ramp. The cap is a safety net, not the schedule — your app still has to pace the actual sends.

Raise the cap each week

When week 2 starts and your ramp climbs, update the same policy in place rather than creating a new one. The update is a PUT /v3/policies/{policy_id} — all fields are optional, so you send only the limit you're changing, nested under limits:

# Raise the daily send cap for week 2 — API
curl -X PUT "https://api.us.nylas.com/v3/policies/<POLICY_ID>" \
  -H "Authorization: Bearer <NYLAS_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "limits": {
      "limit_count_daily_email_sent": 75
    }
  }'
Enter fullscreen mode Exit fullscreen mode

nylas agent policy update takes the policy ID and the same partial JSON body through --data:

# Raise the daily send cap for week 2 — CLI
nylas agent policy update <POLICY_ID> \
  --data '{"limits":{"limit_count_daily_email_sent":75}}'
Enter fullscreen mode Exit fullscreen mode

Repeat at the start of week 3 (300) and week 4 (1000). Because the update is partial, you never have to re-send the rest of the policy — just the new ceiling.

Step 4: The warm-up ramp itself

This is the schedule, straight from the domain warm-up guide. It assumes a daily target of 500–1,000 emails during a typical 8-hour send window, ramped over roughly four weeks. Within each week, nudge the daily number up by 10–20% every day or so:

Week Total emails per day Emails per hour (approx.) Notes
Week 1 5 – 15 1 – 2 Start very low, focus on highly engaged recipients
Week 2 30 – 75 4 – 10 Gradually increase, maintain a steady increase
Week 3 150 – 300 20 – 40 Moderate volume
Week 4 500 – 1,000+ 60 – 125+ Full ramp-up to target volume

Three rules ride alongside the numbers, and they're as load-bearing as the volumes:

  • Spread sends across the day. Don't fire the whole day's batch at once. Distribute across business hours (say 9 AM–5 PM) with randomized delays between messages so the pattern looks human.
  • Send only to real, engaged recipients. This is the single biggest lever, especially in week 1. A reply is a very strong positive signal, a click is a strong one, an open is a mild one — and a spam-mark or a hard bounce hurts. For a brand-new tenant agent, that means seeding the warm-up with people who actually want the mail: the tenant's own team, beta users, internal stakeholders. Tell them to open, click, and reply.
  • Send transactional-style content. Booking confirmations, password resets, account notifications — the kind of mail with high trust and high open rates. No all-caps, no link-stuffing, no sales pitch during warm-up.

Because an Agent Account is a grant, the first warm-up send is the ordinary send call. Here's the API form to an engaged recipient:

# Send a warm-up message from the agent — API
curl -X POST "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/send" \
  -H "Authorization: Bearer <NYLAS_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "to": [{ "name": "Dana Okafor", "email": "dana@acmecustomer.com" }],
    "subject": "Your Acme account is ready",
    "body": "Hi Dana — your workspace is set up. Reply here any time and I will help you get started."
  }'
Enter fullscreen mode Exit fullscreen mode

And the CLI equivalent. The first positional argument is the grant (you can pass the grant_id or the agent's email):

# Send a warm-up message from the agent — CLI
nylas email send <GRANT_ID> \
  --to dana@acmecustomer.com \
  --subject "Your Acme account is ready" \
  --body "Hi Dana — your workspace is set up. Reply here any time and I will help you get started."
Enter fullscreen mode Exit fullscreen mode

One thing I confirmed while writing this: nylas email send has no attachment flag. If your warm-up content needs an attachment, you build it as a draft (nylas email drafts create --attach) or send via the API. For plain warm-up mail you won't miss it.

The scheduling — which N recipients today, the per-message jitter, the daily increase — is your app logic. The warm-up guide ships a Python script you can run daily from cron that handles the pacing for one day's target; adapt it to send through your agent's grant instead of the Transactional Send endpoint, and update the daily target each week to match the table above.

Step 5: Watch bounces and complaints so you can back off

You can't manage deliverability you can't see. While the ramp runs, the thing that tells you whether reputation is holding is the stream of deliverability webhooks Agent Accounts emit: message.delivered, message.bounced, message.complaint, and message.rejected. These are the same events Nylas uses to calculate your bounce and complaint rates — the rates that, if they climb too high, pause sending. Catching a dip in your own telemetry is what keeps you under those thresholds.

Webhooks are application-scoped, not grant-scoped — and that's a feature when you're running many tenants. You subscribe once at the app level, and events for every tenant's agent arrive at the same endpoint, each payload carrying a grant_id you filter on. So you don't re-subscribe per tenant; provision the subscription once and route by grant_id. Subscribe with POST /v3/webhooks:

# Subscribe to deliverability webhooks — API (app-scoped, once)
curl -X POST "https://api.us.nylas.com/v3/webhooks" \
  -H "Authorization: Bearer <NYLAS_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger_types": [
      "message.delivered",
      "message.bounced",
      "message.complaint",
      "message.rejected"
    ],
    "webhook_url": "https://hooks.yourcompany.com/nylas/deliverability"
  }'
Enter fullscreen mode Exit fullscreen mode

The CLI creates the same subscription with --url and --triggers (comma-separated, or repeated flags):

# Subscribe to deliverability webhooks — CLI
nylas webhook create \
  --url https://hooks.yourcompany.com/nylas/deliverability \
  --triggers message.delivered,message.bounced,message.complaint,message.rejected
Enter fullscreen mode Exit fullscreen mode

Run nylas webhook triggers to list every available trigger type if you want to confirm the names yourself. Honest CLI note: the signal you act on — the bounce and complaint events themselves — arrives over the webhook stream and gets handled by your receiver, which is application code, not a CLI command. The CLI helps you set the subscription up and verify deliveries (nylas webhook verify checks a signature locally; nylas webhook server runs a local receiver for testing), but reacting to a complaint spike by lowering the ramp is logic you write.

Wire those events into your back-off rule: when bounces or complaints climb for a tenant's grant_id, slow or pause that tenant's ramp, and stop mailing any address that hard-bounces or complains. One quick implementation note since these matter in production — the API delivers at-least-once (up to three attempts per event), so dedupe on the top-level notification id, which stays constant across retries of the same event. The inner message id identifies the message; the notification id identifies the delivery.

Guardrails: DMARC and per-tenant isolation

Two things to get right before you scale this across tenants.

Publish DMARC, but roll it out in stages. DKIM and SPF are already verified, but DMARC is the layer you add. Publish a record at _dmarc.<your-sending-subdomain> starting at p=none so mail is delivered normally while providers send you aggregate reports — you confirm your agent's mail authenticates and aligns before you tighten anything. Once reports are clean, move to p=quarantine (you can ease in with pct=25), then finally p=reject. Give each stage a couple of weeks of clean reports. Because Agent Accounts sign with your domain's DKIM key and send from your verified domain, alignment works out of the box with relaxed alignment, which is the default they're set up for.

Reputation doesn't transfer between domains. If your model is one domain per tenant, you warm each domain on its own schedule. The reputation a freshly provisioned agent builds is tied to its domain — there's no shared head start across tenants. That's the cost of per-tenant domain isolation, and it's why the warm-up belongs in your provisioning pipeline rather than as a one-time thing you did for the first tenant and forgot about. Every new tenant on a new domain starts the four-week clock over.

What's next

AI-answer pages for agents

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

Top comments (0)