> This page is for Trading.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.immix.xyz/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.immix.xyz/_mcp/server.

# Invite a user

POST /users/invitations
Content-Type: application/json

Invites a person by email (`users:manage`): the row is created `INVITED` with the capabilities stated (scope strings or a `preset`) and the platform emails them; the first sign-in at that verified address claims it. An address or name in use refuses 409 `EMAIL_TAKEN` or `NAME_TAKEN`; a cancelled invitation frees both. Needs an identity provider: the roster answers 503 `INVITATIONS_UNAVAILABLE`.

Reference: https://docs.immix.xyz/trading/api-reference/rest-api/users/invite

## Authentication

- `Authorization` header (bearer token, required) — Bearer authentication of the form `Bearer <token>`, where token is your auth token.

## Request

### Headers

- `Idempotency-Key` (string, required) — Required on every write: a URL-safe string of 1 to 64 characters from `[A-Za-z0-9._~-]` (a UUID fits), scoped to you. It names one operation on one record, ever: a retry replays the stored outcome (`Idempotent-Replay: true`) or attaches to the live one, and reuse on another record refuses 409 `IDEMPOTENCY_KEY_REUSED`. Missing or malformed, 400.
- `X-Request-Id` (string, optional) — Your own id for the request: 8 to 128 of `[A-Za-z0-9._-]`. It becomes the request's id and is echoed on the response. A value off that grammar is replaced with a fresh id, never refused.

### Body (application/json)

This endpoint expects an object.

- `name` (string, required) — The platform name, unique among the organisation's live users.
- `email` (string, required) — The address to invite, lowercased: ASCII, one @, at most 254 characters. Unique among the organisation's invited and active users.
- `expectedVersion` (string, optional, nullable) — int64 as a decimal JSON string
- `capabilities` (list of enum, optional) — The capabilities the invitee holds from the moment they accept. a set (SBE bit-set) served as the array of its scope strings — each choice spelled \<route family>:\<verb> (the schema's camel-cased choice name de-camelled: accountAddressesApprove is account-addresses:approve) — in declared order; empty = none set, never null. The choice set is append-only: a consumer READING this field skips an unknown name, never errors; as input an unknown or repeated name refuses 400 — the parse is strict
  - Allowed values: `users:manage`, `credentials:propose`, `credentials:approve`, `accounts:propose`, `accounts:approve`, `account-addresses:propose`, `account-addresses:approve`, `portfolios:manage`, `asset-policies:propose`, `asset-policies:approve`, `chain-policies:propose`, `chain-policies:approve`, `trading-policies:propose`, `trading-policies:approve`, `orders:trade`, `orgs:propose`, `orgs:approve`, `algos:manage`
- `preset` (enum, optional) — A named capability bundle the edge expands to its scope strings and never forwards — the row records bits, never a preset name: INITIATOR (the six maker-checker families' propose bits), APPROVER (initiator plus their approve bits), ADMIN (approver plus users:manage and portfolios:manage), TRADER (orders:trade), TREASURY (the account, address and asset/chain policy families, both verbs), PLATFORM (orgs:propose and orgs:approve, inside the internal platform org only). Either preset or capabilities names the set; both stated must agree, else 400. x-immix-preset-capabilities is the expansion.
  - Allowed values: `INITIATOR`, `APPROVER`, `ADMIN`, `TRADER`, `TREASURY`, `PLATFORM`
- `expiresInHours` (integer, optional, default: 72) — How long the invitation stays claimable, in hours — the gateway's knob over the wire's `ttlNs`: 1 to 720 (30 days), default 72. The claim deadline is the command's sequencer time plus this lifetime; `kind` is always `HUMAN`.

## Response

### 200

The user as the platform now holds it after the write — the same row the read serves, with the answering change's position, the row's `ETag` and, on a replayed key, `Idempotent-Replay`.

- `capabilities` (list of enum, required) — The user's capabilities — what they may do; empty on a discovered row, and on an invited first administrator until the organisation is activated. a set (SBE bit-set) served as the array of its scope strings — each choice spelled \<route family>:\<verb> (the schema's camel-cased choice name de-camelled: accountAddressesApprove is account-addresses:approve) — in declared order; empty = none set, never null. The choice set is append-only: a consumer READING this field skips an unknown name, never errors; as input an unknown or repeated name refuses 400 — the parse is strict
  - Allowed values: `users:manage`, `credentials:propose`, `credentials:approve`, `accounts:propose`, `accounts:approve`, `account-addresses:propose`, `account-addresses:approve`, `portfolios:manage`, `asset-policies:propose`, `asset-policies:approve`, `chain-policies:propose`, `chain-policies:approve`, `trading-policies:propose`, `trading-policies:approve`, `orders:trade`, `orgs:propose`, `orgs:approve`, `algos:manage`
- `displayName` (string, required) — What the IdP stated at discovery (the name, else email, else nickname claim); informational, never a key.
- `globalSequence` (string, required) — The position as a decimal string — the last change this member had applied, orders, balance readings and algo instances included.
- `kind` (enum, required) — UNKNOWN is the unset sentinel: outputs only — as input, omit the field instead; explicit UNKNOWN refuses 400. The value set is append-only: a consumer READING this field treats an unknown name as UNKNOWN, never as an error; as input an unknown name refuses 400 — the parse is strict
  - Allowed values: `UNKNOWN`, `HUMAN`, `SERVICE`
- `name` (string, required) — Platform name — per-org unique among live users with a non-empty name; empty until admission.
- `orgId` (integer, required) — Membership — stamped by the owner from the actor's org, or from the discovery's parent ref.
- `status` (enum, required) — UNKNOWN is the unset sentinel: outputs only — as input, omit the field instead; explicit UNKNOWN refuses 400. The value set is append-only: a consumer READING this field treats an unknown name as UNKNOWN, never as an error; as input an unknown name refuses 400 — the parse is strict
  - Allowed values: `UNKNOWN`, `DISCOVERED`, `ACTIVE`, `DISABLED`, `INVITED`
- `userId` (integer, required) — Sticky entity key.
- `version` (string, required) — Monotonic per entity. int64 as a decimal JSON string
- `email` (string, optional) — The address the user was invited at, lowercased — the address a first login must have verified to claim the row, kept afterwards as the address the user was registered by. Empty on a row no invitation created. Unique among the organisation's invited and active users (EMAIL_TAKEN).
- `idpInvitationRef` (string, optional) — The identity provider's id for the invitation the platform created, as the platform reported it; informational, never a key; empty until reported.
- `idpUserRef` (string, optional) — The identity provider's subject, opaque, never parsed: a token's sub resolves to this row inside the token's organisation. Empty = unbound (a locally authenticated user). Unique among all the organisation's users, disabled included — a subject binds for the life of its row, and a second binding is refused IDP_REF_TAKEN.
- `invitationState` (enum, optional) — Where the invitation stands: PENDING until the platform has created it with the identity provider, then SENT, ALREADY_MEMBER or FAILED; ACCEPTED once the invitee has signed in; NONE where none was created (a first administrator before activation). Kept after a cancel — an invitation is open while status is INVITED. NONE is the unset sentinel: outputs only — as input, omit the field instead; explicit NONE refuses 400. The value set is append-only: a consumer READING this field treats an unknown name as NONE, never as an error; as input an unknown name refuses 400 — the parse is strict
  - Allowed values: `NONE`, `PENDING`, `SENT`, `ALREADY_MEMBER`, `FAILED`, `ACCEPTED`
- `inviteExpiresAtNs` (string, optional, nullable) — The deadline the row was last invited under: the inviting command's position time plus its stated lifetime; absent — null in the API, 0 on the wire — on a row never invited. Kept after the claim and after a cancel — status and invitationState say whether an invitation is open. A first login after it is refused INVITATION_EXPIRED and a re-invite opens a fresh deadline. int64 as a decimal JSON string
- `invitedByUserId` (integer, optional, nullable) — Who proposed or invited this user: the maker on an organisation's first administrator — stamped at the proposal, whether or not an email follows — and the inviter on an invited member; absent — null in the API, 0 on the wire — on a row created directly or discovered.
- `unscaled` (boolean, optional, deprecated) — Never present since 0.3.3: every money value on this API states its own scale on the wire — a policy amount or cap since 0.3.3, an order's and an execution's since 0.3.2, a balance's since 0.3.1 — so a row is never served raw, whether or not this member holds the asset or instrument it references. Kept, deprecated, so a client generated from 0.3.2 still compiles; it goes at the next major.

## Errors

### 400 Invite Users Request Bad Request Error

Refused at the door — nothing reached the platform and the key was not journaled: a missing or malformed `Idempotency-Key`, a malformed `If-Match` (an absent one is 428 `PRECONDITION_REQUIRED`), malformed JSON, a body `userId`, a body restating the path or `If-Match` differently, a money string off its `pattern` (an exponent, a plus sign, a bare point, surrounding whitespace) or past its `maxLength`, or one no value here can state (over 18 fractional digits, or past a 64-bit integer at its own scale), or any other value outside the schema.

- `error` (ErrorBody, required) — What a refusal states. Since `0.12.0` every refusal carries all four members; a member below it sends `code`, sometimes `message`, and neither of the others.
- `currentVersion` (string, optional) — On 412 `VERSION_CONFLICT`, or `CONFIG_SEQ_CONFLICT` on an algo instance's config write: the row's current version (the `ETag` carries the same token) — re-read, reapply, retry.

### 401 Invite Users Request Unauthorized Error

No bearer token, or one that does not verify.

- `error` (ErrorBody, required) — What a refusal states. Since `0.12.0` every refusal carries all four members; a member below it sends `code`, sometimes `message`, and neither of the others.
- `currentVersion` (string, optional) — On 412 `VERSION_CONFLICT`, or `CONFIG_SEQ_CONFLICT` on an algo instance's config write: the row's current version (the `ETag` carries the same token) — re-read, reapply, retry.

### 403 Invite Users Request Forbidden Error

Authenticated, but not admitted. `NOT_REGISTERED`: no row and no open invitation at your verified address (ask to be invited), an unknown organisation, or a row nobody has claimed (an administrator admits it; no invitation binds a held subject). `EMAIL_UNVERIFIED`: verify your address, then sign in again. `INVITATION_EXPIRED`: an administrator re-invites. `FORBIDDEN_PRINCIPAL`: no `org_id`, a non-ASCII ref, a disabled user, a binding with no user. `ORG_NOT_ACTIVE`: suspended, pending or retired. The record owners' 403s carry the position header and replay with `Idempotent-Replay`.

- `error` (ErrorBody, required) — What a refusal states. Since `0.12.0` every refusal carries all four members; a member below it sends `code`, sometimes `message`, and neither of the others.
- `currentVersion` (string, optional) — On 412 `VERSION_CONFLICT`, or `CONFIG_SEQ_CONFLICT` on an algo instance's config write: the row's current version (the `ETag` carries the same token) — re-read, reapply, retry.

### 409 Invite Users Request Conflict Error

The record owner's refusal, by class: 409 for a state or uniqueness refusal, 422 for a field or reference refusal — a fact on the platform's stream, so it carries the position header and a retried key replays it. The same statuses carry door refusals, never journaled — a retry takes fresh: 409 `IDEMPOTENCY_KEY_REUSED`; 422 for a reference this member will not send — `INSTRUMENT_NOT_LIVE` or `ACCOUNT_NOT_ACTIVE` on `POST /orders`, `REFERENCE_UNKNOWN_INSTRUMENT`, `REFERENCE_INSTRUMENT_NOT_LIVE` or `REFERENCE_UNKNOWN_ACCOUNT` in an algo document; a key the member holds answers as its retry.

- `error` (ErrorBody, required) — What a refusal states. Since `0.12.0` every refusal carries all four members; a member below it sends `code`, sometimes `message`, and neither of the others.
- `currentVersion` (string, optional) — On 412 `VERSION_CONFLICT`, or `CONFIG_SEQ_CONFLICT` on an algo instance's config write: the row's current version (the `ETag` carries the same token) — re-read, reapply, retry.

### 413 Invite Users Request Content Too Large Error

The request is too large: a body declared past 1 MB (`PAYLOAD_TOO_LARGE`), or a command past the platform's own bound (`TOO_LARGE`, not expected for any route here). Nothing was submitted; a retry takes fresh.

- `error` (ErrorBody, required) — What a refusal states. Since `0.12.0` every refusal carries all four members; a member below it sends `code`, sometimes `message`, and neither of the others.
- `currentVersion` (string, optional) — On 412 `VERSION_CONFLICT`, or `CONFIG_SEQ_CONFLICT` on an algo instance's config write: the row's current version (the `ETag` carries the same token) — re-read, reapply, retry.

### 422 Invite Users Request Unprocessable Entity Error

The record owner's refusal, by class: 409 for a state or uniqueness refusal, 422 for a field or reference refusal — a fact on the platform's stream, so it carries the position header and a retried key replays it. The same statuses carry door refusals, never journaled — a retry takes fresh: 409 `IDEMPOTENCY_KEY_REUSED`; 422 for a reference this member will not send — `INSTRUMENT_NOT_LIVE` or `ACCOUNT_NOT_ACTIVE` on `POST /orders`, `REFERENCE_UNKNOWN_INSTRUMENT`, `REFERENCE_INSTRUMENT_NOT_LIVE` or `REFERENCE_UNKNOWN_ACCOUNT` in an algo document; a key the member holds answers as its retry.

- `error` (ErrorBody, required) — What a refusal states. Since `0.12.0` every refusal carries all four members; a member below it sends `code`, sometimes `message`, and neither of the others.
- `currentVersion` (string, optional) — On 412 `VERSION_CONFLICT`, or `CONFIG_SEQ_CONFLICT` on an algo instance's config write: the row's current version (the `ETag` carries the same token) — re-read, reapply, retry.

### 429 Invite Users Request Too Many Requests Error

Shed at once: `INBOX_FULL` or `IN_FLIGHT_FULL` (this member's bounds; nothing queued, the key not journaled), the intake's or the ticket route's `RATE_LIMITED`, or the orders owner's `RATE_CEILING` (the organisation's per-minute ceiling — a refusal on the stream, replayed like any, so its `retryable` is `false` and the order goes again under a fresh key). `Retry-After` says when.

- `error` (ErrorBody, required) — What a refusal states. Since `0.12.0` every refusal carries all four members; a member below it sends `code`, sometimes `message`, and neither of the others.
- `currentVersion` (string, optional) — On 412 `VERSION_CONFLICT`, or `CONFIG_SEQ_CONFLICT` on an algo instance's config write: the row's current version (the `ETag` carries the same token) — re-read, reapply, retry.

### 500 Invite Users Request Internal Server Error

The gateway failed to serve (`INTERRUPTED`, `INTERNAL_ERROR`, `PROJECTION_MISS`); the operation record, if one exists, is still readable.

- `error` (ErrorBody, required) — What a refusal states. Since `0.12.0` every refusal carries all four members; a member below it sends `code`, sometimes `message`, and neither of the others.
- `currentVersion` (string, optional) — On 412 `VERSION_CONFLICT`, or `CONFIG_SEQ_CONFLICT` on an algo instance's config write: the row's current version (the `ETag` carries the same token) — re-read, reapply, retry.

### 503 Invite Users Request Service Unavailable Error

This member cannot carry the write now: `NOT_PRIMED` (still catching up), `NOT_SERVING` (a read-only or not-ready member, or one that could not look the request's key up within the deadline), `OWNER_ABSENT` (the record owner has no active member), `INTAKE_UNAVAILABLE` (no credential store here), `INVITATIONS_UNAVAILABLE` (the roster: no identity provider), `REGISTRATION_PENDING` (your invitation's claim is in flight — retry after `Retry-After`), or the platform's own outcome for a command lost in transit. Nothing was journaled; retry against a serving member.

- `error` (ErrorBody, required) — What a refusal states. Since `0.12.0` every refusal carries all four members; a member below it sends `code`, sometimes `message`, and neither of the others.
- `currentVersion` (string, optional) — On 412 `VERSION_CONFLICT`, or `CONFIG_SEQ_CONFLICT` on an algo instance's config write: the row's current version (the `ETag` carries the same token) — re-read, reapply, retry.

## Types

### ErrorBody

What a refusal states. Since `0.12.0` every refusal carries all four members; a member below it sends `code`, sometimes `message`, and neither of the others.

- `code` (string, required) — The code to branch on — stable and append-only: treat one you do not know by its HTTP status. The gateway's: - 400: `MALFORMED_REQUEST`, `MISSING_IDEMPOTENCY_KEY`, `INVALID_IDEMPOTENCY_KEY`, `INVALID_IF_MATCH`, `INVALID_CURSOR` - 401: `UNAUTHENTICATED` - 403: `FORBIDDEN_PRINCIPAL`, `NOT_REGISTERED`, `INVITATION_EXPIRED`, `EMAIL_UNVERIFIED`, `ORG_NOT_ACTIVE`, `CAPABILITY_DENIED`, `AWAITING_ADMISSION` - 404: `NOT_FOUND`, `UNKNOWN_OPERATION`, `ROUTE_NOT_FOUND` (no route answers the path) - 405: `METHOD_NOT_ALLOWED` - 409: `IDEMPOTENCY_KEY_REUSED`, `INTAKE_KEY_REUSED` - 413: `TOO_LARGE`, `PAYLOAD_TOO_LARGE`, `DOCUMENT_TOO_LARGE` - 422: a reference this member will not send: `INSTRUMENT_NOT_LIVE`, `ACCOUNT_NOT_ACTIVE` (`POST /orders`), `REFERENCE_UNKNOWN_INSTRUMENT`, `REFERENCE_INSTRUMENT_NOT_LIVE`, `REFERENCE_UNKNOWN_ACCOUNT` (an algo document); `VENUE_UNSUPPORTED`; `SCALE_UNKNOWN` (retired at `0.3.4`) - 428: `PRECONDITION_REQUIRED` - 429: `INBOX_FULL`, `IN_FLIGHT_FULL`, `RATE_LIMITED` - 500: `INTERRUPTED`, `INTERNAL_ERROR`, `PROJECTION_MISS` - 503: `NOT_PRIMED`, `NOT_SERVING`, `OWNER_ABSENT`, `INTAKE_UNAVAILABLE`, `BALANCES_UNAVAILABLE`, `ALGOS_UNAVAILABLE`, `TICKETS_UNAVAILABLE`, `INVITATIONS_UNAVAILABLE`, `REGISTRATION_PENDING`, `LOST`, `REJECTED_NOT_REGISTERED`, `REJECTED_NOT_ACTIVE` The record owners' refusals ride verbatim — `OrgRegisterRejected.reason`, `OrderRejected.reason` and `AlgoRejected.reason` list them; the guide maps each to its status.
- `message` (string, required) — For display only; its wording may change at any time.
- `requestId` (string, required) — The request's id, also sent as X-Request-Id; quote it to support.
- `retryable` (boolean, required) — Whether sending the identical request again, its `Idempotency-Key` included, after `Retry-After` or a short pause, can succeed with nothing else changed. `false` where something must change first: the request, its key, its precondition, the caller's standing, or the platform's own records or deployment. A refusal the record owner judged is always `false`: it is the recorded answer to its key, which replays it.

## Examples

**Request**

```json
{
  "name": "name",
  "email": "email"
}
```

**Response**

```json
{
  "capabilities": [
    "users:manage"
  ],
  "displayName": "displayName",
  "globalSequence": "globalSequence",
  "kind": "UNKNOWN",
  "name": "name",
  "orgId": 1,
  "status": "UNKNOWN",
  "userId": 1,
  "version": "version",
  "email": "email",
  "idpInvitationRef": "idpInvitationRef",
  "idpUserRef": "idpUserRef",
  "invitationState": "NONE",
  "inviteExpiresAtNs": "inviteExpiresAtNs",
  "invitedByUserId": 1
}
```

**SDK Code**

```python
import requests

url = "https://api.example.com/users/invitations"

payload = {
    "name": "name",
    "email": "email"
}
headers = {
    "Idempotency-Key": "Idempotency-Key",
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.example.com/users/invitations';
const options = {
  method: 'POST',
  headers: {
    'Idempotency-Key': 'Idempotency-Key',
    Authorization: 'Bearer <token>',
    'Content-Type': 'application/json'
  },
  body: '{"name":"name","email":"email"}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.example.com/users/invitations"

	payload := strings.NewReader("{\n  \"name\": \"name\",\n  \"email\": \"email\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Idempotency-Key", "Idempotency-Key")
	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.example.com/users/invitations")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Idempotency-Key"] = 'Idempotency-Key'
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"name\": \"name\",\n  \"email\": \"email\"\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.example.com/users/invitations")
  .header("Idempotency-Key", "Idempotency-Key")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"name\": \"name\",\n  \"email\": \"email\"\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.example.com/users/invitations', [
  'body' => '{
  "name": "name",
  "email": "email"
}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
    'Idempotency-Key' => 'Idempotency-Key',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.example.com/users/invitations");
var request = new RestRequest(Method.POST);
request.AddHeader("Idempotency-Key", "Idempotency-Key");
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"name\": \"name\",\n  \"email\": \"email\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "Idempotency-Key": "Idempotency-Key",
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [
  "name": "name",
  "email": "email"
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.example.com/users/invitations")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```