Create or update a user
Creating and updating a user are the same operation and share the same UpsertUserRequest body. POST …/users creates the user — or updates them if the email already exists. PUT …/users/{uid} updates a known user by UID. Both return 200.
Before you start, make sure you have a valid access token — see the Overview for authentication details.
Step 1 — Check whether the user exists
Use the lookup reference to search for the user by email or name. A hit means you will be updating an existing user; a miss means you will be creating a new one.
Step 2 — Gather the UIDs to assign
Collect the group and permission UIDs you want to assign to the user. See the lookup reference for how to retrieve them from the groups and permissions endpoints.
Step 3 — Send the request
Create or upsert by email
POST /v2/organizations/DEMO/users
- Python
- PHP
- JavaScript
- Java
- Go
- C#
import requests
BASE_URL = "https://api.stonal.io/users"
TOKEN = "<access_token>"
resp = requests.post(
f"{BASE_URL}/v2/organizations/DEMO/users",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"email": "john.doe@example.com",
"firstName": "John",
"lastName": "Doe",
"userGroupUids": ["019619df-4768-76b7-81e3-2c56d374df46"],
"permissions": [{"uid": "019619df-4767-730f-8d31-143712a08141"}],
},
)
print(resp.status_code, resp.json())
<?php
$baseUrl = "https://api.stonal.io/users";
$token = "<access_token>";
$ch = curl_init("$baseUrl/v2/organizations/DEMO/users");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode([
"email" => "john.doe@example.com",
"firstName" => "John",
"lastName" => "Doe",
"userGroupUids" => ["019619df-4768-76b7-81e3-2c56d374df46"],
"permissions" => [["uid" => "019619df-4767-730f-8d31-143712a08141"]],
]),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . PHP_EOL . $response;
curl_close($ch);
const baseUrl = "https://api.stonal.io/users";
const token = "<access_token>";
const res = await fetch(`${baseUrl}/v2/organizations/DEMO/users`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({
email: "john.doe@example.com",
firstName: "John",
lastName: "Doe",
userGroupUids: ["019619df-4768-76b7-81e3-2c56d374df46"],
permissions: [{ uid: "019619df-4767-730f-8d31-143712a08141" }],
}),
});
console.log(res.status, await res.json());
import java.net.URI;
import java.net.http.*;
String baseUrl = "https://api.stonal.io/users";
String token = "<access_token>";
String body = """
{"email":"john.doe@example.com","firstName":"John","lastName":"Doe",
"userGroupUids":["019619df-4768-76b7-81e3-2c56d374df46"],
"permissions":[{"uid":"019619df-4767-730f-8d31-143712a08141"}]}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v2/organizations/DEMO/users"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
baseURL := "https://api.stonal.io/users"
token := "<access_token>"
body := []byte(`{"email":"john.doe@example.com","firstName":"John","lastName":"Doe","userGroupUids":["019619df-4768-76b7-81e3-2c56d374df46"],"permissions":[{"uid":"019619df-4767-730f-8d31-143712a08141"}]}`)
req, _ := http.NewRequest("POST", baseURL+"/v2/organizations/DEMO/users", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode, string(out))
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var baseUrl = "https://api.stonal.io/users";
var token = "<access_token>";
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var json = """
{"email":"john.doe@example.com","firstName":"John","lastName":"Doe",
"userGroupUids":["019619df-4768-76b7-81e3-2c56d374df46"],
"permissions":[{"uid":"019619df-4767-730f-8d31-143712a08141"}]}
""";
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync($"{baseUrl}/v2/organizations/DEMO/users", content);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
Update by UID
PUT /v2/organizations/DEMO/users/019619df-4768-76b7-81e3-2c56d374df46
- Python
- PHP
- JavaScript
- Java
- Go
- C#
import requests
BASE_URL = "https://api.stonal.io/users"
TOKEN = "<access_token>"
resp = requests.put(
f"{BASE_URL}/v2/organizations/DEMO/users/019619df-4768-76b7-81e3-2c56d374df46",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"email": "john.doe@example.com",
"firstName": "John",
"lastName": "Doe",
"userGroupUids": ["019619df-4768-76b7-81e3-2c56d374df46"],
"permissions": [{"uid": "019619df-4767-730f-8d31-143712a08141"}],
},
)
print(resp.status_code, resp.json())
<?php
$baseUrl = "https://api.stonal.io/users";
$token = "<access_token>";
$ch = curl_init("$baseUrl/v2/organizations/DEMO/users/019619df-4768-76b7-81e3-2c56d374df46");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode([
"email" => "john.doe@example.com",
"firstName" => "John",
"lastName" => "Doe",
"userGroupUids" => ["019619df-4768-76b7-81e3-2c56d374df46"],
"permissions" => [["uid" => "019619df-4767-730f-8d31-143712a08141"]],
]),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . PHP_EOL . $response;
curl_close($ch);
const baseUrl = "https://api.stonal.io/users";
const token = "<access_token>";
const res = await fetch(`${baseUrl}/v2/organizations/DEMO/users/019619df-4768-76b7-81e3-2c56d374df46`, {
method: "PUT",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({
email: "john.doe@example.com",
firstName: "John",
lastName: "Doe",
userGroupUids: ["019619df-4768-76b7-81e3-2c56d374df46"],
permissions: [{ uid: "019619df-4767-730f-8d31-143712a08141" }],
}),
});
console.log(res.status, await res.json());
import java.net.URI;
import java.net.http.*;
String baseUrl = "https://api.stonal.io/users";
String token = "<access_token>";
String body = """
{"email":"john.doe@example.com","firstName":"John","lastName":"Doe",
"userGroupUids":["019619df-4768-76b7-81e3-2c56d374df46"],
"permissions":[{"uid":"019619df-4767-730f-8d31-143712a08141"}]}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v2/organizations/DEMO/users/019619df-4768-76b7-81e3-2c56d374df46"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.PUT(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
baseURL := "https://api.stonal.io/users"
token := "<access_token>"
body := []byte(`{"email":"john.doe@example.com","firstName":"John","lastName":"Doe","userGroupUids":["019619df-4768-76b7-81e3-2c56d374df46"],"permissions":[{"uid":"019619df-4767-730f-8d31-143712a08141"}]}`)
req, _ := http.NewRequest("PUT", baseURL+"/v2/organizations/DEMO/users/019619df-4768-76b7-81e3-2c56d374df46", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode, string(out))
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var baseUrl = "https://api.stonal.io/users";
var token = "<access_token>";
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var json = """
{"email":"john.doe@example.com","firstName":"John","lastName":"Doe",
"userGroupUids":["019619df-4768-76b7-81e3-2c56d374df46"],
"permissions":[{"uid":"019619df-4767-730f-8d31-143712a08141"}]}
""";
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PutAsync($"{baseUrl}/v2/organizations/DEMO/users/019619df-4768-76b7-81e3-2c56d374df46", content);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
Request body
{
"email": "john.doe@example.com",
"firstName": "John",
"lastName": "Doe",
"fromExternalIdp": false,
"allAssets": false,
"userGroupUids": ["019619df-4768-76b7-81e3-2c56d374df46"],
"permissions": [
{ "uid": "019619df-4767-730f-8d31-143712a08141" },
{ "uid": "019619df-4768-76b3-8ab3-4414dcf29ff1" }
]
}
Field reference
| Field | Required | Type | Notes |
|---|---|---|---|
email | ✅ | string | Unique; immutable on update |
firstName | ✅ | string | |
lastName | ✅ | string | |
uid | — | string | Identifies the user on update |
password | — | string | ≥ 8 chars; upper + lower + digit + special |
fromExternalIdp | — | boolean | Controls the activation email (see warning) |
allAssets | — | boolean | Grant all assets instead of listing ASSET permissions |
userGroupUids | — | string[] | Group UIDs |
permissions | — | { uid }[] | Permission UIDs |
fromExternalIdpThe fromExternalIdp flag is what controls whether Stonal sends the new user an account-activation email (the "Set your STONAL password" message). It is the only setting that governs this behaviour for the API — the organization's front-end SSO toggle has no effect on it.
fromExternalIdp: false(the default) — the account is created with a local password and the user receives the activation email so they can set or confirm their password. If you omit the field, it defaults tofalse, so the email is sent.fromExternalIdp: true— the account is treated as federated: no activation email is sent and the user must authenticate through their external identity provider (SSO).
If your users sign in through SSO and you do not want them to receive the "Set your STONAL password" email, you must explicitly pass "fromExternalIdp": true in the create call. Leaving it at false (or omitting it) for SSO users is the most common cause of unexpected activation emails.
Updating vs creating
PUTrequires the user'suidin the URL path.emailcannot be changed on an existing user.- There are no partial updates — send the full set of fields you want on the user.
- The provided
permissionsanduserGroupUidsreplace the existing ones; any previously assigned groups or permissions not included in the new request will be removed.
Worked end-to-end example
This example walks through three lookups to collect UIDs, then creates a user.
1. Find the group to assign
GET /v1/organizations/DEMO/groups
Abbreviated response:
{
"content": [
{ "uid": "019619df-4768-76b7-81e3-2c56d374df46", "name": "Analysts" }
]
}
Group UID to use: 019619df-4768-76b7-81e3-2c56d374df46
2. Find the profile permission
GET /v1/organizations/DEMO/users/permissions?type=PROFILE
Abbreviated response:
{
"content": [
{ "uid": "019619df-4767-730f-8d31-143712a08141", "name": "Standard profile" }
]
}
Profile permission UID: 019619df-4767-730f-8d31-143712a08141
3. Find the portfolio permission
GET /v1/organizations/DEMO/users/permissions?type=ASSET&subType=PORTFOLIO
Abbreviated response:
{
"content": [
{ "uid": "019619df-4768-76b3-8ab3-4414dcf29ff1", "name": "All portfolios" }
]
}
Portfolio permission UID: 019619df-4768-76b3-8ab3-4414dcf29ff1
4. Create the user
The three UIDs from the lookups feed directly into the request body:
POST /v2/organizations/DEMO/users
{
"email": "john.doe@example.com",
"firstName": "John",
"lastName": "Doe",
"fromExternalIdp": false,
"allAssets": false,
"userGroupUids": ["019619df-4768-76b7-81e3-2c56d374df46"],
"permissions": [
{ "uid": "019619df-4767-730f-8d31-143712a08141" },
{ "uid": "019619df-4768-76b3-8ab3-4414dcf29ff1" }
]
}
5. Response
200 OK
The user is created (or updated if the email already existed) and assigned to the Analysts group with the standard profile and all-portfolios permissions.
Responses
200— user created or updated successfully.
Stonal APIs return a consistent error envelope: { "type", "title", "detail" }. Validation failures (422) replace detail with a per-field errors array.
| Status | type | Meaning |
|---|---|---|
400 | tag:InvalidBody / tag:InvalidContentType | The request body or content type is invalid |
401 | tag:Unauthenticated | Missing or expired authentication token |
403 | tag:ForbiddenAccess | The token lacks permission for this resource |
422 | tag:ValidationError | One or more fields failed validation (see errors[]) |
500 | tag:InternalError | Unexpected server error |
{
"type": "tag:ValidationError",
"title": "Invalid request",
"errors": [
{ "field": "email", "detail": "Email is required" }
]
}