Spanly Docs

Identity resolver protocol

The HTTP contract between Spanly producers and your token-to-user resolver endpoint. Version 1.

For opaque tokens, or when you want custom logic, identity resolution calls an HTTP endpoint you host that maps a token to a user. This page specifies that protocol (v1).

The caller is always a producer running inside your infrastructure: the Spanly CLI proxy (spanly run --port / spanly proxy), the TypeScript SDK middleware, and on the roadmap the hosted Spanly Gateway, which will speak this same protocol. Your users' raw tokens never leave your perimeter, and Spanly's backend never calls this endpoint.

Request

The producer sends one POST per unknown token:

POST <configured URL>
Content-Type: application/json
X-Spanly-Identity-Secret: <shared secret>

{"token": "<raw bearer credential>", "mcpSessionId": "<session id>"}
  • token is the credential from the Authorization: Bearer header, verbatim.
  • mcpSessionId is optional context. Under the sessionless MCP spec (2026-07-28) it may be omitted entirely, or carry a Spanly-synthetic value prefixed spanly-. Resolvers must not depend on it.

The URL is whatever you configure; any route works, and the endpoint can live on the same server as the MCP itself.

Response

CaseResponse
Known token200 with a user object (below)
Unknown token200 with body null, or 404
Anything elseTreated as a transient failure

User object, only id is required:

{
  "id": "usr_123",
  "email": "ada@example.com",
  "name": "Ada Lovelace",
  "accountId": "acct_42",
  "accountName": "Acme Corp"
}

id must be your stable internal user id, not the token and not a rotating claim: it is the attribution key, so rotating tokens keep pointing at the same user. accountId and accountName enable company-level attribution.

Caller behavior (normative)

The producer:

  • times out after 3 seconds and does not retry; a failed token is retried naturally when its negative cache entry expires (about 60 seconds),
  • makes a single in-flight request per token, so concurrent traffic with the same token piggybacks on the pending resolution,
  • caches a resolved identity for about 15 minutes per token, so token rotation and producer restarts do not hammer the endpoint,
  • fails open: resolution failure never affects MCP serving; the sessions simply stay unattributed until a later retry succeeds.

Your obligations

  • Verify X-Spanly-Identity-Secret with a constant-time comparison.
  • Treat token as the sensitive credential it is: do not log it.
  • Serve HTTPS in production (http:// is fine for localhost development).

Example (Express)

const express = require('express');
const { timingSafeEqual } = require('node:crypto');

const app = express();
app.use(express.json());

const SECRET = process.env.SPANLY_IDENTITY_SECRET;

app.post('/spanly-identity', async (req, res) => {
  const given = Buffer.from(req.get('X-Spanly-Identity-Secret') ?? '');
  const expected = Buffer.from(SECRET);
  if (given.length !== expected.length || !timingSafeEqual(given, expected)) {
    return res.status(401).end();
  }
  const session = await sessions.findByApiToken(req.body.token);
  if (!session) return res.status(404).end();
  res.json({
    id: session.userId,
    email: session.userEmail,
    name: session.userName,
    accountId: session.orgId,
    accountName: session.orgName,
  });
});

app.listen(4000);

Point the CLI at it:

spanly run --port 3000 \
  --identity-resolver-url=http://localhost:4000/spanly-identity \
  --identity-resolver-secret=$SPANLY_IDENTITY_SECRET \
  -- node server.js

On this page