from operate_as import OperateAs
from operate_as_sheets import Sheets
operate = OperateAs("oa_••••")
miller = operate.client("b2f4c3e1-…")
rows = Sheets(miller).read_rows(
spreadsheet_id="1BxiM…",
range="Bookings!A2:C5",
)Quickstart
Any framework, same tools
Your code, an AI agent or an n8n workflow all use one key and reach the same clients and tools. Pick how you connect: each tab installs it, creates the client with your key, lists your clients and reads rows from one client's sheet.
The Python library, published as operate-as, with a typed package for each service. No dependencies beyond the standard library.
- Each service's package types its actions, so your editor completes each input
- Errors are typed, so you can catch ReconnectRequiredError and alert your team
- Sheets(miller).request(...) calls any endpoint of the service's API
pip install operate-as operate-as-sheetsimport os
from operate_as import OperateAs
from operate_as_sheets import Sheets
# Create the client once, with the key from the environment
operate = OperateAs(os.environ["OPERATE_AS_API_KEY"])
# One of your clients, by the id the dashboard shows
miller = operate.client("6f1c2a7e-...")
# Read rows from their sheet
rows = Sheets(miller).read_rows(spreadsheet_id="1BxiM...", range="A1:D20")
print(rows.status, rows.data["values"])The Node library, published as operate-as, with a typed package for each service. No dependencies: it uses the built-in fetch.
- Each service's package types its actions, so your editor completes each input
- Errors are typed, so you can catch ReconnectRequiredError and alert your team
- new Sheets(miller).request(...) calls any endpoint of the service's API
npm install operate-as @operate-as/sheetsimport { OperateAs } from "operate-as";
import { Sheets } from "@operate-as/sheets";
// Create the client once, with the key from the environment
const operate = new OperateAs(process.env.OPERATE_AS_API_KEY);
// One of your clients, by the id the dashboard shows
const miller = operate.client("6f1c2a7e-...");
// Read rows from their sheet
const rows = await new Sheets(miller).readRows({ spreadsheetId: "1BxiM...", range: "A1:D20" });
console.log(rows.status, rows.data);The Go module, github.com/arcarolabs/operate-as-go, with a typed module for each service. No dependencies beyond the standard library.
- Each service's module types its actions, so the compiler checks each input
- operateas.IsReconnectRequired(err) tells you when a client needs to reconnect
- miller.Request(...) calls any endpoint of the service's API
go get github.com/arcarolabs/operate-as-go \
github.com/arcarolabs/operate-as-go/services/sheetspackage main
import (
"context"
"fmt"
"os"
operateas "github.com/arcarolabs/operate-as-go"
"github.com/arcarolabs/operate-as-go/services/sheets"
)
func main() {
ctx := context.Background()
// Create the client once, with the key from the environment
operate := operateas.New(os.Getenv("OPERATE_AS_API_KEY"))
// One of your clients, by the id the dashboard shows
miller := sheets.New(operate, "6f1c2a7e-...")
// Read rows from their sheet
rows, err := miller.ReadRows(ctx, sheets.ReadRowsInput{SpreadsheetID: "1BxiM...", Range: "A1:D20"})
if err != nil {
panic(err)
}
fmt.Println(rows.Status, string(rows.Data))
}Plain HTTPS from any language. Every request goes to https://api.operate.as/v1 with your key in the Authorization header.
- Errors are JSON with one field, detail, written to be shown to a person
- Pass-through: any path under /v1/clients/{clientId}/{service}/ goes to the service's API
export OPERATE_AS_API_KEY="oa_..."# The clients this key can reach, and what each has connected
curl https://api.operate.as/v1/me/clients \
-H "Authorization: Bearer $OPERATE_AS_API_KEY"
# Read rows from one client's sheet
curl -X POST https://api.operate.as/v1/actions/sheets.read_rows \
-H "Authorization: Bearer $OPERATE_AS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"clientId": "6f1c2a7e-...",
"input": { "spreadsheet_id": "1BxiM...", "range": "A1:D20" }
}'Our MCP server speaks streamable HTTP at https://api.operate.as/v1/mcp, with the same Authorization header. Your agent gets list_clients, search_tools, get_tool, run_tool and request, plus a tool for each action of the services your clients connected.
- A scoped key means the agent can only reach the clients, services and actions you allowed
- Failures come back as results the agent can read, such as a client needing to reconnect
claude mcp add --transport http operate-as https://api.operate.as/v1/mcp \
--header "Authorization: Bearer oa_..."// "Read the first rows of Miller Dental's bookings sheet"
// The agent finds the client and what they've connected
{ "name": "list_clients", "arguments": {} }
// Then reads rows from their sheet
{
"name": "sheets_read_rows",
"arguments": {
"client_id": "6f1c2a7e-...",
"spreadsheet_id": "1BxiM...",
"range": "A1:D20"
}
}Install the community node n8n-nodes-operate-as in self-hosted n8n (Settings, then Community nodes), and add an “Operate As API” credential with your key. The Client and Action dropdowns fill live from the API.
- Choose Custom Request in the node to call any endpoint of the service's API
- Before the node is verified for n8n cloud, the built-in HTTP Request node does the same job
# Settings > Community nodes > Install
npm package name: n8n-nodes-operate-as
# Credentials > New > Operate As API
API Key: oa_...Credential: Operate As API
Client: Miller Dental
Operation: Run an Action
Action: "sheets: Read rows from a range of a spreadsheet"
Inputs:
spreadsheet_id: 1BxiM...
range: A1:D20Tools
64,000+ tools your agents can call
Every service comes with ready-made tools for its API, plus a request tool for any endpoint they don't cover. Pick a service and search what an agent can do with it.
- Loading every tool…
requestAny other Stripe endpoint, called with the client's accesshttps://api.stripe.com/…
On every key
search_toolsFinds the right tool from a few words, so an agent never loads thousands at once
get_toolReads one tool's inputs, with their full JSON Schemas, before running it
run_toolRuns any tool it found for one client, with that client's access attached
requestCalls any endpoint of a service's own API, for anything the tools don't cover
list_clientsShows which clients this key reaches, and what each has connected
Over MCP, a tool's dots become underscores: sheets.read_rows is sheets_read_rows. In n8n you pick it from a list instead.
HTTPS API
Ready-made actions, or any endpoint
Use a ready-made action for the common jobs, or pass any request through to the service's own API. Either way we attach the client's access on our server and hand back the service's response.
Endpoints
- GET /v1/me/clients
- The clients this key can reach, with the services each has connected.
- GET /v1/actions
- Ready-made actions a page at a time, each with its inputs as
name,description,typeandrequired. Every one is listed under Tools above. - POST /v1/actions/{name}
- Run an action for one client. Send
clientIdandinput, with input names in snake_case such asspreadsheet_id(camelCase works too). Get back the service's ownstatusanddata. - ANY /v1/clients/{clientId}/{service}/{path}
- Pass-through to any endpoint of the service's API, for example
GET /v1/clients/{id}/sheets/v4/spreadsheets/{spreadsheetId}. - POST /v1/keys
- A short-lived key for one agent's run. It reaches no more than your key, narrowed further with
clientIds,servicesandactions, and stops afterexpiresInseconds (an hour by default) or the moment your key is revoked.
Errors, in plain words
Every error is JSON with one field, {"detail": "..."}, written to be shown to a person.
- 401
- The key is wrong, revoked, or a short-lived key that expired.
- 402
- The workspace has no plan, such as after a trial ends. Choose one in the dashboard.
- 403
- The request is outside the clients, services, actions or addresses this key allows, or your plan.
- 404
- There's no such action or client.
- 409
- The client hasn't connected this service yet, or their access ended. Send them their link to reconnect.
- 429
- Your plan allows no more calls this period.
Retry-Aftersays how many seconds until you can try again. - 502
- The service didn't answer. Try again shortly.
- 503
- The provider can't renew the client's access right now. Try again later.
List actions
curl "https://api.operate.as/v1/actions?service=sheets" \
-H "Authorization: Bearer $OPERATE_AS_API_KEY"
{
"items": [
{
"name": "sheets.read_rows",
"service": "sheets",
"description": "Read rows from a range of a spreadsheet",
"inputs": [
{
"name": "spreadsheet_id",
"description": "The ID in the sheet's address, after /d/",
"type": "string",
"required": true
},
{ "name": "range", "description": "Which cells, e.g. Sheet1!A1:D20", "type": "string", "required": true }
]
},
...
],
"page": 1,
"pageSize": 50,
"total": 19
}Pass-through to any endpoint
curl https://api.operate.as/v1/clients/6f1c2a7e-.../sheets/v4/spreadsheets/1BxiM... \
-H "Authorization: Bearer $OPERATE_AS_API_KEY"
{
"status": 200,
"data": { "spreadsheetId": "1BxiM...", "sheets": [...] }
}A short-lived key for an agent
curl -X POST https://api.operate.as/v1/keys \
-H "Authorization: Bearer $OPERATE_AS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"expiresIn": 900, "services": ["sheets"]}'
{
"secret": "oa_tmp_Xr4q...",
"expiresAt": "2026-10-19T13:15:00Z",
"services": ["sheets"]
}Catching reconnect required
HTTP/1.1 409 Conflict
{
"detail": "Miller Dental hasn't connected Google yet. Send them their link."
}API keys
- Create keys on the dashboard's API & webhooks page
- Keys start with
oa_and are shown once; we keep only a hash - Scope a key to specific clients, services and actions
- Lock a key to your servers' IP addresses, so a copy is refused anywhere else
- Hand agents short-lived
oa_tmp_keys, so they never hold one that lasts - Revoke a key at any time, and it stops working at once
- A key never carries a client's token, so a leaked key exposes only what it was scoped to
Security
Your clients' tokens never leave us
Every call passes through Operate As, which attaches the client's access on our server. So if your code or a workflow export ever leaks, it holds one key you can revoke, and nothing of your clients'.
How we handle your clients' dataNotifications
Know before a workflow breaks
We check every connection daily, so dead access turns up before your workflow runs into it. If a call does reach an ended connection, it returns 409 with a reconnect message instead of failing silently. The reconnect link is the client's same original link.
Sent to your agency the moment something changes.
Dashboard
A notification beside the client it's about.
Webhooks
Signed JSON to an HTTPS URL you set on the dashboard's API & webhooks page.
Events
- connection.connected
- The client approved access to a service.
- connection.expired
- Renewal failed: the client revoked access in the provider, changed their password, an admin blocked it, or it went unused for six months.
- connection.removed
- Your team disconnected it, or the client removed access from their page.
- connection.account_changed
- The client connected a different account from the one before.
- connection.limited
- The client paused or resumed a connection, or allowed only reads.
- client.passkey_saved
- The client saved a passkey, so they can connect what you asked for.
- client.completed
- The client connected everything you asked for.
- api_key.new_address
- An API key was used from an address it hadn't been used from before.
- api_key.refused_address
- An API key was tried from outside the addresses it's locked to, and refused.
- webhook.test
- Sent when you press Send test on the API & webhooks page, to check your endpoint.
Point the webhook at an n8n Webhook trigger to pause the client's workflows, alert your Slack, or email the client their reconnectUrl.
Webhook delivery
POST https://your-server.example/hooks/operate-as
Operate-As-Signature: t=1760620800,v1=5f2b...
{
"id": "evt_...",
"type": "connection.expired",
"createdAt": "2026-10-16T08:00:00+00:00",
"data": {
"clientId": "6f1c2a7e-...",
"clientName": "Miller Dental",
"provider": "google",
"services": ["sheets", "gmail"],
"reconnectUrl": "https://..."
}
}Check it really came from us
Anyone can send a request to your webhook's address, so check each one before acting on it. With our libraries it's one line.
- 01
Keep your webhook secret
It's shown once when you save the webhook on the API & webhooks page, and starts with whsec_. Store it like a password, in an environment variable.
- 02
Pass the request as it arrived
Give the check the body as raw text, before you parse the JSON, and the Operate-As-Signature header. Parsing first changes the text, and the check fails.
- 03
Act only if it passes
You get the event back when it's real. It's refused if the signature doesn't match, or if it was sent more than five minutes ago, so an old request can't be replayed.
import os
from operate_as import SignatureVerificationError, Webhook
def handle(raw_body: bytes, headers: dict[str, str]) -> int:
"""raw_body: the request body bytes exactly as received"""
try:
event = Webhook.verify(
raw_body,
headers["Operate-As-Signature"],
os.environ["OPERATE_AS_WEBHOOK_SECRET"], # whsec_...
)
except SignatureVerificationError:
return 400 # not from us: ignore it
if event["type"] == "connection.expired":
email_client(event["data"]["reconnectUrl"])
return 204Get started
Send your next client one link
Add a client, select the services, copy the link. You'll get an email the moment their access is ready. Start free, no card required. Paid plans are one flat monthly price, and a busy month never takes your agents offline.