curl --request POST \
--url https://api.opal.dev/v1/service-users \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "paladin-prod",
"owner_id": "7c86c85d-0651-43e2-a748-d69d658418e8"
}
'import requests
url = "https://api.opal.dev/v1/service-users"
payload = {
"name": "paladin-prod",
"owner_id": "7c86c85d-0651-43e2-a748-d69d658418e8"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({name: 'paladin-prod', owner_id: '7c86c85d-0651-43e2-a748-d69d658418e8'})
};
fetch('https://api.opal.dev/v1/service-users', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.opal.dev/v1/service-users",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'paladin-prod',
'owner_id' => '7c86c85d-0651-43e2-a748-d69d658418e8'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.opal.dev/v1/service-users"
payload := strings.NewReader("{\n \"name\": \"paladin-prod\",\n \"owner_id\": \"7c86c85d-0651-43e2-a748-d69d658418e8\"\n}")
req, _ := http.NewRequest("POST", url, payload)
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(string(body))
}HttpResponse<String> response = Unirest.post("https://api.opal.dev/v1/service-users")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"paladin-prod\",\n \"owner_id\": \"7c86c85d-0651-43e2-a748-d69d658418e8\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.opal.dev/v1/service-users")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"paladin-prod\",\n \"owner_id\": \"7c86c85d-0651-43e2-a748-d69d658418e8\"\n}"
response = http.request(request)
puts response.read_body{
"user_id": "29827fb8-f2dd-4e80-9576-28e31e9934ac",
"email": "john.doe@company.dev",
"full_name": "John Doe",
"first_name": "John",
"last_name": "Doe",
"position": "Engineer"
}Create service user
Creates a new service user (a non-human identity owned by a team). Mirrors
the GraphQL createServiceUser mutation; admin-only.
The returned User object is the newly created service user and is a
regular User row — read it later via GET /users/{user_id}, delete it
with DELETE /users/{user_id}, and manage its tags via the usual
/users/{user_id}/tags endpoints.
Role grants are a separate concern. To grant a service user access
to resources or groups, use POST /role-assignments (or the per-pair
endpoints such as /resources/{id}/users/{id}). Keeping role
assignments out of this request body means each grant stays
independently addressable — the shape Terraform and other declarative
clients prefer, with no field here that would silently drift when a
grant is added or revoked through a different code path.
Automations are not configurable here either. The GraphQL mutation accepts a wide typed-union of automation configuration; that surface is intentionally omitted from this REST endpoint for the same reason. Service users created through this endpoint start with no automations.
curl --request POST \
--url https://api.opal.dev/v1/service-users \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "paladin-prod",
"owner_id": "7c86c85d-0651-43e2-a748-d69d658418e8"
}
'import requests
url = "https://api.opal.dev/v1/service-users"
payload = {
"name": "paladin-prod",
"owner_id": "7c86c85d-0651-43e2-a748-d69d658418e8"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({name: 'paladin-prod', owner_id: '7c86c85d-0651-43e2-a748-d69d658418e8'})
};
fetch('https://api.opal.dev/v1/service-users', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.opal.dev/v1/service-users",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'paladin-prod',
'owner_id' => '7c86c85d-0651-43e2-a748-d69d658418e8'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.opal.dev/v1/service-users"
payload := strings.NewReader("{\n \"name\": \"paladin-prod\",\n \"owner_id\": \"7c86c85d-0651-43e2-a748-d69d658418e8\"\n}")
req, _ := http.NewRequest("POST", url, payload)
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(string(body))
}HttpResponse<String> response = Unirest.post("https://api.opal.dev/v1/service-users")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"paladin-prod\",\n \"owner_id\": \"7c86c85d-0651-43e2-a748-d69d658418e8\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.opal.dev/v1/service-users")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"paladin-prod\",\n \"owner_id\": \"7c86c85d-0651-43e2-a748-d69d658418e8\"\n}"
response = http.request(request)
puts response.read_body{
"user_id": "29827fb8-f2dd-4e80-9576-28e31e9934ac",
"email": "john.doe@company.dev",
"full_name": "John Doe",
"first_name": "John",
"last_name": "Doe",
"position": "Engineer"
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
Request body for POST /service-users. Intentionally minimal: only the
two fields that uniquely identify a new service user. Role grants are
a separate resource managed through /role-assignments, which keeps
each grant independently addressable and avoids silent drift when
grants are added or revoked through another code path.
The display name of the service user.
1"paladin-prod"
The ID of the owner (team of record) that owns the service user. Must reference an existing owner in the caller's organization.
"7c86c85d-0651-43e2-a748-d69d658418e8"
Response
The created service user.
User Object
Description
The User object is used to represent a user.
Usage Example
Fetch from the LIST Sessions endpoint.
The ID of the user.
"h0z968412-2451-4bbd-42h4-057l715d917m"
The email of the user.
"john.doe@company.dev"
The full name of the user.
"John Doe"
The first name of the user.
"John"
The last name of the user.
"Doe"
The user's position.
"Senior Engineer"
The ID of the user's manager. Null if the user has no manager.
"29827fb8-f2dd-4e80-9576-28e31e9934ac"
User status pulled from an HR/IDP provider.
ACTIVE, SUSPENDED, DEPROVISIONED, DELETED, NOT_FOUND "ACTIVE"
Was this page helpful?