dataset.
Writes follow the Studio’s draft model. documents.create and documents.edit change the draft (drafts.<id>), and readers of published content see nothing until documents.publish. Pass publish: true to do both in one transaction. documents.patch and documents.mutate write the exact ids you give them, so a published id changes the live document at once.
Before writing a type you don’t know, call schemas.list. It returns each deployed schema’s types and fields, and the schemaId the Agent Actions need. A project deploys its schema with sanity schema deploy, and the list only shows what was last deployed.
The Agent Actions (generate, transform, translate, prompt) use one Sanity AI credit per call and stop with a 429 when the organization’s AI budget runs out. They write to drafts unless forcePublishedWrite is set. noWrite: true returns the result without saving it. Sanity marks these actions experimental, and they run on API version vX.
The plugin retries reads after a 5xx or a network failure. It never repeats a write that may have landed. Sanity’s 429 for too many requests means nothing ran, so those are retried for writes too.
What you get:
- 26 typed API operations
- 1 incoming webhook event types
Setup
1
Install
2
Add the plugin
corsair.ts
multiTenancy: true, scope calls with corsair.withTenant(id). See Quick start for KEK + Hub keys, and Multi-tenancy for account isolation.3
Choose authentication
- API Key (Recommended)
No setup required yet. When you make your first request as a tenant, Corsair prompts for the API key.More: API Key
4
Connect a tenant
Mint a connect link and send the tenant to it. Hub hosts the page and delivers the result to your app — see Connect / OAuth.
Example API calls
List the latest published postsWebhooks
Create the webhook withwebhooks.create, or under API > Webhooks at sanity.io/manage, and give it a secret. Store the same secret with keys.set_webhook_signature. Corsair keeps one secret per tenant, so all of one tenant’s webhooks must share it. sanity({ webhookSecret }) covers accounts with no stored secret, and a stored one takes precedence. Use that option only in a single-tenant app. Every tenant without a stored secret falls back to it, and then a delivery signed for one tenant also verifies under another’s tenantId. Deliveries without a valid signature are refused. Hand processWebhook the raw body text (await request.text()), because a parsed body cannot verify. A delivery that fails verification makes processWebhook report a failure. Answer it with a 5xx and Sanity retries twice, 30 seconds apart. Signatures more than 15 minutes from now are refused. Deliveries can repeat, so deduplicate by idempotencyKey. The event’s body is the webhook’s GROQ projection, and its operation, document id and dataset come from Sanity’s headers. On Corsair’s cloud, Hub routes each delivery by its sanity-project-id header to the account that stored that project id. In your own handler, processWebhook takes the tenant from ?tenantId= and uses default without it, so in a multi-tenant app give each tenant its own webhook with ?tenantId= on the URL and pass Object.fromEntries(new URL(request.url).searchParams) to processWebhook as the fourth argument. The signature covers the body but not the URL, so give each tenant’s webhook its own secret. A delivery replayed under another tenantId then fails that tenant’s check. A before hook runs ahead of the signature check, so keep side effects in after.
What’s next
API reference
Every
sanity.api.* operation with input and output types.Webhooks
Event paths, payloads, and
webhookHooks examples.Connect / OAuth
createLink, Hub delivery, and tenant connect flows.
Use with agents
Expose this plugin’s operations as MCP tools.