> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upsolve.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Let Upsolve act as your users

> Connect your own API so each user's chat only reaches their own account: a server key with bindings, OAuth, or a token callback.

An **API connector** lets the workspace's AI call your REST API. When the person asking is one of your embedded users, the call must be made as them, never as someone else. You choose how Upsolve does that when you add the connector (Connections → API).

| Your API authenticates with... | Choose | What you build |
| - | - | - |
| A server key that can act for any account (common in B2B) | **Server key + user binding** (recommended) | Nothing. |
| OAuth, one login per user | **OAuth** | Nothing in your API: you register Upsolve as an OAuth client. |
| Per-user session tokens only | **Token callback** | One signed endpoint that returns a short-lived token. |
| Nothing (a public API) | **None** | Nothing. Bindings still apply. |

## Server key and user binding (recommended)

Keep one API key on the connector. It is stored encrypted and only ever sent to your API's base URL. A **binding** tells Upsolve which parameter names the user, and which property supplies its value:

| Field | Meaning |
| - | - |
| `in` | Where the value goes: `header`, `query` or `path`. |
| `name` | The parameter's name, for example `X-Account-Id` or `userId`. |
| `source` | `projectOrganization` or `projectUser`. |
| `property` | The key of a property your host already sends. |

The properties are the ones you pass to [`POST /v1/api/projects/embed-token`](https://docs.upsolve.ai/api-reference/endpoint/create-embed-token) in `organization.properties` and `user.properties`, the same ones row-level security reads. If your API wants the account id in a header, send it once at sign-in and bind it:

```json embed-token (excerpt) theme={null}
{
  "organization": { "externalId": "acme", "properties": { "account_id": "acct_4821" } },
  "user": { "externalId": "u_123", "properties": { "external_user_id": "u_123" } }
}
```

```json Binding theme={null}
{ "in": "header", "name": "X-Account-Id", "source": "projectOrganization", "property": "account_id" }
```

How it behaves:

* **The model never chooses the user.** A bound parameter is removed from the tool's schema and set by Upsolve on every call. If the model supplies one anyway, it is dropped.
* **Properties are read on the server**, not from the token in the browser, so a user cannot change them.
* **It fails closed.** A user who has no value for a bound property gets none of the connector's tools, with the reason shown ("Your account has no `account_id`.").
* **Operations that do not take every bound parameter are hidden**, unless you mark them global (a plan list, a lookup table). A server key can act for any account, so a call without its binding could read across customers.
* The property must be a string, number or boolean. An empty or missing value counts as missing.

> [!NOTE]
>
> Upsolve enforces the binding; your API does not see who is asking. It is the same trust as giving Upsolve database credentials with row-level security: the key can act for any account, and Upsolve only ever sends the caller's own.

## OAuth

Enter your OAuth client ID and secret, the authorization and token endpoints (prefilled from your OpenAPI spec when it declares them) and the scopes. Each user signs in once in a popup. Upsolve stores their tokens encrypted and refreshes them, so rules and scheduled work keep running. If a refresh is refused, the user sees "needs to be reconnected" and their rules pause until they connect again. The client secret is only sent to your token endpoint.

## Token callback

Use this when your API only accepts tokens issued per user and has no server key. Upsolve asks **your endpoint** for a token each time it needs one for a user.

When you create the connector, Upsolve shows a **callback secret** (`upsk_...`) once. Store it on your server. You can rotate it from the connector's settings, which invalidates the old one at once. **Test callback** calls your endpoint as you and reports the status, latency and token lifetime. The token itself is never shown.

### The request

```http theme={null}
POST https://api.example.com/upsolve/token
X-Upsolve-Timestamp: 1761200000
X-Upsolve-Signature: v1=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>
Content-Type: application/json
User-Agent: Upsolve-Token-Callback/1.0

{"externalUserId":"u_123","externalOrganizationId":"acct_4821","projectUserId":"…","projectId":"…","workspaceId":"…","scopes":["invoices:read"],"requestId":"…"}
```

* `externalUserId` and `externalOrganizationId` are the `externalId` values you sent to `embed-token`. They are `null` for a member of your Upsolve organization who is not one of your embedded users.
* `scopes` are the scopes you configured on the connector.
* The timestamp is Unix seconds. The signature is the lowercase hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the callback secret.

### Your response

```json theme={null}
{ "accessToken": "…", "expiresIn": 900, "tokenType": "Bearer" }
```

`accessToken` and `expiresIn` (seconds, up to 7 days) are required. `tokenType` defaults to `Bearer`. The token is sent as `Authorization: Bearer <token>` unless you configured another header and prefix. The answer must be JSON under 64 KB.

### What to check on your side

1. **Verify the signature over the raw body before parsing it.** Re-serialised JSON will not match.
2. **Reject timestamps more than 300 seconds from your clock.**
3. **Compare in constant time.**
4. **Look the user up by `externalUserId`.** If you do not know them, or they are not entitled to this API, answer with a `4xx`.
5. **Return a short-lived token** for that user only.

### Failures

| Your endpoint answers | What happens |
| - | - |
| `4xx` | The user is not let in: the connector's tools are unavailable to them, and a rule using it pauses with a lost-connection reason. Not retried. |
| `5xx`, no answer within 10 seconds, or a body that is not a valid token | Treated as temporary: that chat call fails once, and a rule counts one failure. |

### Caching

Upsolve keeps each token in memory for its user and connector until 60 seconds before it expires (at most 24 hours), so your endpoint is called roughly once per token lifetime per user. Simultaneous requests for one user share a single call. Tokens are never written to a database or a log, and rotating the secret clears them.

### Samples

Both samples are complete, tested endpoints you can copy: [`examples/api-connector-token-callback`](https://github.com/Upsolve-Labs/upsolve-core/tree/main/examples/api-connector-token-callback) has a Node (Express) and a Python (FastAPI) version, with the contract in its README. The verification function in each:

```javascript Node theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyUpsolveSignature({ secret, timestamp, signature, rawBody, nowSeconds }) {
  if (!/^\d{1,12}$/.test(timestamp ?? "")) return false;
  if (Math.abs(nowSeconds - Number(timestamp)) > 300) return false;
  const match = /^v1=([0-9a-f]{64})$/.exec(signature ?? "");
  if (!match) return false;
  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody) // the Buffer from express.raw(), not parsed JSON
    .digest();
  return timingSafeEqual(Buffer.from(match[1], "hex"), expected);
}
```

```python Python theme={null}
import hashlib, hmac, re, time

def verify_upsolve_signature(secret, timestamp, signature, raw_body: bytes) -> bool:
    if not re.fullmatch(r"[0-9]{1,12}", timestamp or ""):
        return False
    if abs(time.time() - int(timestamp)) > 300:
        return False
    match = re.fullmatch(r"v1=([0-9a-f]{64})", signature or "")
    if not match:
        return False
    expected = hmac.new(
        secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(match.group(1), expected)  # raw_body = await request.body()
```

Replace the sample's `mintTokenFor` / `mint_token_for` stub with your own user lookup and token issuer.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.