Machine-readable descriptions
The REST surface is described by an OpenAPI 3.1 document at /openapi.json, and by the same document as YAML at /api/openapi.yaml. Every operation carries a unique operationId, a prose description, typed parameters, and a response schema, so the document converts straight into function-calling tool definitions with no hand-editing. /api/v1 is an unauthenticated index of every documented entry point, the authentication methods, the quotas, and this versioning policy. Fetch it first if you are deciding whether to integrate at all.
The MCP surface is described by the Server Card at /.well-known/mcp/server-card.json, to the SEP-1649 discovery standard, and by /api/v1/capabilities, which lists every tool, resource, and prompt with the current application manifest contract and a worked example. /llms.txt says when SpringRoll is the right thing to reach for and when it is not; /llms-full.txt is every public page concatenated as markdown. Every page on this site is also available as markdown. Send Accept: text/markdown and you get it, with Vary: Accept on the response.
Authentication
The preferred route needs no token and no form. Point an OAuth-capable MCP client at the endpoint /api/mcp: an unauthenticated tool call answers 401 with a WWW-Authenticate challenge naming the authorization server, the client registers itself through RFC 7591 dynamic client registration, the user authorizes in a browser, and the client receives its own credential. The metadata is at /.well-known/oauth-authorization-server (RFC 8414) and /.well-known/oauth-protected-resource (RFC 9728). Nothing is ever pasted between windows, and no one has to be emailed.
For a client that cannot speak OAuth, a SpringRoll agent token, prefixed sbp_, goes in the Authorization header as a bearer credential. Tokens are self-serve: a member creates one from Settings, Coding agents, inside their organization, and it is displayed exactly once. A token is issued to a named membership and inherits exactly that person's permissions, so it can never record an approval decision. A bearer token takes precedence over a session cookie, so a script running inside a logged-in browser cannot accidentally act as the user.
A tenant id or slug in a URL path is never authorization on its own. It is always verified against the caller's memberships, and a token used against an organization it does not belong to is refused with FORBIDDEN rather than quietly rescoped.
Rate limits
Every rate-limited response carries its quota in headers, so a client can pace itself instead of discovering the limit by hitting it. Three vocabularies are sent, because clients disagree about which to read: the structured fields RateLimit and RateLimit-Policy (for example, RateLimit: "read";r=598;t=42), the RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset triplet, and the older X-RateLimit-* convention. RateLimit-Reset is a delta in seconds; X-RateLimit-Reset is a Unix timestamp. A 429 adds Retry-After and repeats the whole set, and the error body's details name the policy, the limit, and the window.
The quotas, per 60-second window unless stated otherwise: 600 reads, 120 writes, 20 deployments, and 30 requests to unauthenticated endpoints. Dynamic client registration is tighter at 10 per hour, because it is an unauthenticated write that creates a durable record. A SpringRoll Connect data grant carries its own per-application queries-per-minute limit, set by the data owner who approved it. Limits are keyed on the authenticated identity where there is one and on the network address otherwise, so one organization behind a shared egress address cannot exhaust another's budget.
The unauthenticated discovery documents (/api/v1, /api/v1/capabilities, /openapi.json, /api/openapi.yaml, and /api/health) advertise the quotas in a RateLimit-Policy header without charging you for a request, so the budget is readable before the first integration call. A browser-based client can read all of these cross-origin: every rate-limit header is named in Access-Control-Expose-Headers.
Versioning and deprecation
The major version is the first path segment after /api, and v1 is current. Within a version only additive changes are made, including new fields, new optional parameters, and new endpoints. Treat an unknown response field as forward compatibility rather than as an error. A breaking change gets a new path segment; the version you integrated against does not change underneath you.
A version being retired keeps answering for at least 365 days from the announcement. From the moment it is deprecated, every response it returns carries a Deprecation header (RFC 9745) with the date it was deprecated, a Sunset header (RFC 8594) with the date it stops answering, and a Link header with rel="deprecation" pointing at this page and rel="successor-version" pointing at what to move to. After the sunset date the version answers 410 Gone with the standard error envelope. Nothing is deprecated today, which is exactly why the mechanism is published now: a client written against v1 has to already know to look for those headers before the first one appears.
MCP tool names follow a different rule, and pretending otherwise would be the more dangerous documentation. They are versioned by the server version rather than by a path, and MCP has no deprecation channel for a tool name. A renamed or removed tool therefore fails loudly on tools/call rather than aliasing quietly to something with different behaviour. Call tools/list at the start of a session instead of hard-coding names.
Errors
Every endpoint answers one envelope: an error object with a stable code, a human-readable message, a correlationId, and, when the failure is recoverable, a details object and an ordered nextActions list. Branch on code, never on the message text and never on the HTTP status, which several codes share. Read nextActions before retrying: for a POLICY_BLOCKED it names the blocking policy, and for a RATE_LIMITED it names the window you are in.
The correlationId identifies the request in the organization's audit log, is echoed in the x-correlation-id response header, and is honoured on the way in if you send your own. A trace started in your system therefore carries through ours. Quote it when reporting a problem: for a 5xx the message is deliberately generic, and the correlation id is the only way to find the real failure. Unknown paths under /api answer 404 with this same envelope rather than with HTML, and unknown paths elsewhere answer a real 404 whose body points back at these indexes.
What you can call before signing up
Before you have an account, five endpoints answer in full: /api/v1, /api/v1/capabilities, /openapi.json, /api/openapi.yaml, and /api/health. Between them they describe every tool, every REST operation, the manifest contract with a worked example, and the quotas.
The MCP endpoint also completes a handshake unauthenticated. Send initialize, then tools/list, resources/list, or prompts/list to /api/mcp with no credential, and you get the real catalog and the same server instructions a connected client sees. Anything that would touch an organization's data (tools/call, resources/read, prompts/get) answers 401 with the WWW-Authenticate challenge that starts the browser authorization flow. A client can therefore decide whether SpringRoll is worth connecting to before it asks anyone to authorize anything.
To exercise writes, use a real organization on the Direct to Production workflow with a throwaway application: deployments are cheap, an application can be retired, and every action is attributable in the audit log. There is no separate sandbox tenant with synthetic data. If your procurement review needs one, write to us.
Clients and tooling
Claude Code has a bundled SpringRoll skill, installed with npx skills add springroll-dev/springroll-skill --skill springroll. It is worth having over a bare MCP connection because it can read the working tree, archive it, and resolve the git remote, which a prompt alone cannot do. Any other OAuth-capable MCP client, including Cursor, Codex, ChatGPT, and Claude Desktop, connects to the same endpoint with no extra software.
Deployments are asynchronous and there is no outbound webhook: a call returns as soon as it has a deployment id, and you poll springroll.deploy.status, or GET the deployment, until the runtime reports READY. Send an Idempotency-Key on every write you might retry. The same key with the same body replays the original result, and with a different body it is refused, so a timeout can never produce a duplicate deployment.
