Hand an agent a task like "build me a recipe blog" and it can scaffold the app, write the content, and ship it. Then it hits a wall. The CMS wants an account, the account wants an email address, and the verification code lands in a human's mail client. The run stops and waits for someone to paste six digits back.
This tutorial removes that stop. The agent provisions its own email inbox from AgentMail, passes that address to Cosmic agent signup as human_email, reads the claim code out of its own inbox, and submits it to verify. Working curl for every step, and no human in the loop.
Why it is worth wiring up: until that code is submitted, the new project sits in a restricted state with a deletion clock running on it. An agent that can read its own mail clears the gate itself and finishes the run holding a verified Cosmic project, a bucket, and the read and write keys to start writing content to it.
This is for developers building an agent (Cursor, Claude, a backend service) that needs a real content backend and can be handed an email address. You need a domain you can receive mail on, and nothing else.
The flow below was run against the production APIs on September 22, 2026. Signup returned an unclaimed project, the claim code arrived in the AgentMail message preview, and verify returned auth_type: verified with the restricted limits lifted.
What you get at the end
- An AgentMail inbox at
my-agent@agentmail.to, plus an API key the agent uses to read it - A Cosmic project and bucket in
auth_type: verified - Bucket
read_keyandwrite_key, ready to pass to the SDK - An
agent_key(agk_...) for later status checks
Between signup and verify the bucket sits in an unclaimed state, which you pass straight through rather than stop in. While unclaimed it allows 50 objects and 5 MB of media, AI generation is off, and the project is hard-deleted after 14 days if nobody claims it. Verifying lifts all of that.
Reference docs: Cosmic agent signup and the AgentMail quickstart.
Two different email addresses
This is the part that trips people up, so settle it before touching any code. Two addresses are in play and they do different jobs.
1. The AgentMail owner address. A real mailbox on a normal domain, something like you@yourdomain.com. You pass it as human_email on POST /v0/agent/sign-up, once, the first time this human creates an AgentMail account. AgentMail sends its own 6-digit OTP there.
2. The agent inbox. Created by that signup, in the form username@agentmail.to. This is the address you hand to Cosmic as human_email. Cosmic's claim email lands here, and the agent reads it with the AgentMail API key.
Using one address for both jobs fails. AgentMail rejects @agentmail.to as an owner address, and pointing Cosmic at your personal mailbox puts the claim code somewhere the agent cannot reach.
Create the AgentMail inbox
curl -X POST https://api.agentmail.to/v0/agent/sign-up \
-H "Content-Type: application/json" \
-d '{
"human_email": "you@yourdomain.com",
"username": "my-agent",
"source": "cursor"
}'
The response carries api_key, inbox_id, and organization_id. With username: "my-agent" the inbox is my-agent@agentmail.to, and inbox_id is that same email address. Store the API key somewhere durable, because it is not shown again.
AgentMail also emails a 6-digit OTP to you@yourdomain.com. Submit it to unlock full permissions on the key:
curl -X POST https://api.agentmail.to/v0/agent/verify \
-H "Authorization: Bearer $AGENTMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "otp_code": "123456" }'
The field here is otp_code. Keep that in mind, because Cosmic's verify endpoint names the same concept differently in a later step.
One observation from the September 22 run: the key returned by signup was already able to list inbox messages before this OTP was submitted. Do not build on that. AgentMail documents the key as limited until verification, so treat verifying as part of setup.
Errors you will actually hit
-
@agentmail.toas the owner address returns403withcode: "forbidden"and the messageDomain is forbidden. The owner address has to be an ordinary mailbox. -
Disposable inbox domains return the same
403andforbiddencode. Use a real domain you control. -
An address that already has an AgentMail account returns
403withcode: "already_exists"and the messageUser already exists, and no new API key comes back. Plus-aliases count as the same user, soyou+agent@yourdomain.comcollides withyou@yourdomain.com. When the human already has an account, skip signup entirely: use their existing API key and create the mailbox withPOST /v0/inboxes.
Branch on the code field rather than the message text. AgentMail documents those codes as stable and the prose as subject to change.
Sign up for Cosmic with the agent inbox
curl -X POST https://dapi.cosmicjs.com/v3/agents/sign-up \
-H "Content-Type: application/json" \
-d '{
"human_email": "my-agent@agentmail.to",
"project_name": "Recipe Blog",
"agent_id": "cursor"
}'
Here human_email has to be the AgentMail inbox. The owner's personal address belongs only to the AgentMail signup in the previous step.
The response contains agent_key (prefixed agk_), access_token, auth_type: "unclaimed", the project, bucket.slug, bucket.read_key, bucket.write_key, a claim_url, and the limits object. Persist the agent_key and the bucket keys. If your sample code writes state to disk, keep those values out of anything you log.
Cosmic then emails the agent inbox. The subject reads An AI agent created a Cosmic project for you: "Recipe Blog", and the plain-text body contains the line Your one-time claim code is 123456 (expires in 15 minutes).
Two behaviors worth coding for:
- Calling signup again with the same
human_emailandagent_idis idempotent. You get the same project back and a fresh code, which is the correct recovery path for an expired code. - If that email already belongs to a human Cosmic account, the response is
409withcode: "user_already_exists"and aclaim_existing_url. Stop there and surface the URL. Retrying with a different email is the wrong move and leaves stray projects behind.
Read the claim code from the inbox
curl "https://api.agentmail.to/v0/inboxes/my-agent@agentmail.to/messages?subject=Cosmic&limit=20" \
-H "Authorization: Bearer $AGENTMAIL_API_KEY"
The inbox_id path segment is the email address itself. The subject query parameter does a substring match server side, so the agent can ask for the Cosmic message directly instead of scanning everything that arrives. Drop the filter if you would rather poll the whole list and match client side.
Messages come back newest first, each with message_id, subject, and an optional preview. In the live run the 6-digit code was sitting in preview. When the preview is empty or cut short, fetch the single message at GET /v0/inboxes/{inbox_id}/messages/{message_id} and read its text body.
Match on the labeled code rather than the first six digits on the page, because a timestamp or an ID can beat the real code to the regex:
const match = text.match(/claim code is\s*(\d{6})/i)
const code = match?.[1]
The code expires after 15 minutes. If your poll loop runs past that, call Cosmic signup again with the same email and agent_id and read the fresh code.
Verify the Cosmic project
curl -X POST https://dapi.cosmicjs.com/v3/agents/verify \
-H "Authorization: Bearer $COSMIC_AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "123456" }'
Cosmic names this field code. AgentMail named the equivalent field otp_code back in step one. Sending the wrong key is an easy mistake when both calls sit in the same script.
A successful call returns Verified. Restricted-mode limits lifted. along with auth_type: "verified". Confirm it independently:
curl https://dapi.cosmicjs.com/v3/agents/status \
-H "Authorization: Bearer $COSMIC_AGENT_KEY"
auth_type reads verified and limits comes back null. The bucket is now on standard free-plan limits, AI generation included.
Use the bucket
From here it is an ordinary Cosmic bucket, with one step that trips people up. A bucket created by agent signup is empty. It has no Object types in it, and an Object cannot exist without a type to live in. So the agent models the content first, then writes to it.
Pass the slug, read_key, and write_key from the signup response to createBucketClient. The write key is what authorizes both calls below:
import { createBucketClient } from '@cosmicjs/sdk'
const cosmic = createBucketClient({
bucketSlug: process.env.COSMIC_BUCKET_SLUG,
readKey: process.env.COSMIC_READ_KEY,
writeKey: process.env.COSMIC_WRITE_KEY
})
Create the Object type. Only title is required, and the slug defaults to the title converted to a slug. Declare the fields the agent intends to write in metafields:
await cosmic.objectTypes.insertOne({
title: 'Posts',
singular: 'Post',
slug: 'posts',
emoji: '📝',
metafields: [
{
title: 'Content',
key: 'content',
type: 'markdown',
required: false
}
]
})
Now the object write succeeds, because type: 'posts' resolves to the type that exists and the content key in metadata matches a metafield on it:
await cosmic.objects.insertOne({
type: 'posts',
title: 'Hello from an agent',
metadata: { content: 'Written by an agent that signed itself up.' }
})
Two things to handle if this runs unattended. Object type slugs are unique per bucket, so an agent that might run more than once should call cosmic.objectTypes.find() first and create the type only when it is missing. And any key you set in metadata has to exist as a metafield on the type, so adding a field later means a cosmic.objectTypes.updateOne() call before the write rather than a new key in the object payload. The Object types reference covers the full set of parameters, and the Metafields reference lists every field type available.
The agent signup docs cover the rest of the surface: refreshing access_token, handing the claim_url to a human when one is available, and the 402 with agent_unclaimed_limit that you hit if verification gets skipped and the agent keeps writing.
The whole point of this setup is that the claim code never has to pass through a person. An agent that can receive email can provision its own content backend, verify it, and start writing, in one uninterrupted run.
Ready to try it? Start free or read the agent API docs. If you are wiring agents into a larger content operation, grab time with our CEO.
Originally published on the Cosmic blog.
Top comments (0)