curl --request POST \
--url https://api.nexspace365.com/api/oauth-clients \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"clientName": "Ops automation",
"redirectUris": [
"https://ops.example.com/callback"
],
"scopes": [
"shifts:read",
"staff:read"
],
"isPublic": true
}
'import requests
url = "https://api.nexspace365.com/api/oauth-clients"
payload = {
"clientName": "Ops automation",
"redirectUris": ["https://ops.example.com/callback"],
"scopes": ["shifts:read", "staff:read"],
"isPublic": True
}
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({
clientName: 'Ops automation',
redirectUris: ['https://ops.example.com/callback'],
scopes: ['shifts:read', 'staff:read'],
isPublic: true
})
};
fetch('https://api.nexspace365.com/api/oauth-clients', 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.nexspace365.com/api/oauth-clients",
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([
'clientName' => 'Ops automation',
'redirectUris' => [
'https://ops.example.com/callback'
],
'scopes' => [
'shifts:read',
'staff:read'
],
'isPublic' => true
]),
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.nexspace365.com/api/oauth-clients"
payload := strings.NewReader("{\n \"clientName\": \"Ops automation\",\n \"redirectUris\": [\n \"https://ops.example.com/callback\"\n ],\n \"scopes\": [\n \"shifts:read\",\n \"staff:read\"\n ],\n \"isPublic\": true\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.nexspace365.com/api/oauth-clients")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"clientName\": \"Ops automation\",\n \"redirectUris\": [\n \"https://ops.example.com/callback\"\n ],\n \"scopes\": [\n \"shifts:read\",\n \"staff:read\"\n ],\n \"isPublic\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.nexspace365.com/api/oauth-clients")
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 \"clientName\": \"Ops automation\",\n \"redirectUris\": [\n \"https://ops.example.com/callback\"\n ],\n \"scopes\": [\n \"shifts:read\",\n \"staff:read\"\n ],\n \"isPublic\": true\n}"
response = http.request(request)
puts response.read_body{
"id": 123,
"clientId": "<string>",
"clientName": "<string>",
"redirectUris": [
"<string>"
],
"isPublic": true,
"scopes": [
"<string>"
],
"metadata": {},
"registeredById": 123,
"registeredByName": "<string>",
"registrationSource": "dashboard",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"clientSecret": "<string>"
}{
"error": {
"message": "<string>",
"code": "<string>",
"details": {},
"suggestion": "<string>",
"retryable": true
},
"requestId": "<string>"
}{
"error": {
"message": "Invalid or revoked API key",
"code": "UNAUTHENTICATED",
"suggestion": "Send a valid `Authorization: Bearer <token>` (nex_live_/nex_pat_ key, JWT, or OAuth access token).",
"retryable": false
}
}{
"error": {
"message": "<string>",
"code": "<string>",
"details": {},
"suggestion": "<string>",
"retryable": true
},
"requestId": "<string>"
}Register an OAuth client from the dashboard
Create an OAuth client with an operator behind it. This is the
dashboard-managed counterpart to the public RFC 7591
POST /oauth/register: the row records registeredById, so it reports
registrationSource: dashboard while self-registered clients report
dynamic.
Scopes are silently filtered. Submitted scopes are passed through
filterKnownHeadlessScopes before storage — anything outside the
headless catalog is dropped without an error. If every submitted scope
is unknown, the request fails with 400 (“At least one valid scope is
required”). Check the scopes array on the response to see what was
actually granted.
clientSecret is returned exactly once, and only for confidential
clients (isPublic: false). Public clients (the default) use PKCE and
get no secret.
Requires the system.manage_integrations permission.
curl --request POST \
--url https://api.nexspace365.com/api/oauth-clients \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"clientName": "Ops automation",
"redirectUris": [
"https://ops.example.com/callback"
],
"scopes": [
"shifts:read",
"staff:read"
],
"isPublic": true
}
'import requests
url = "https://api.nexspace365.com/api/oauth-clients"
payload = {
"clientName": "Ops automation",
"redirectUris": ["https://ops.example.com/callback"],
"scopes": ["shifts:read", "staff:read"],
"isPublic": True
}
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({
clientName: 'Ops automation',
redirectUris: ['https://ops.example.com/callback'],
scopes: ['shifts:read', 'staff:read'],
isPublic: true
})
};
fetch('https://api.nexspace365.com/api/oauth-clients', 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.nexspace365.com/api/oauth-clients",
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([
'clientName' => 'Ops automation',
'redirectUris' => [
'https://ops.example.com/callback'
],
'scopes' => [
'shifts:read',
'staff:read'
],
'isPublic' => true
]),
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.nexspace365.com/api/oauth-clients"
payload := strings.NewReader("{\n \"clientName\": \"Ops automation\",\n \"redirectUris\": [\n \"https://ops.example.com/callback\"\n ],\n \"scopes\": [\n \"shifts:read\",\n \"staff:read\"\n ],\n \"isPublic\": true\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.nexspace365.com/api/oauth-clients")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"clientName\": \"Ops automation\",\n \"redirectUris\": [\n \"https://ops.example.com/callback\"\n ],\n \"scopes\": [\n \"shifts:read\",\n \"staff:read\"\n ],\n \"isPublic\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.nexspace365.com/api/oauth-clients")
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 \"clientName\": \"Ops automation\",\n \"redirectUris\": [\n \"https://ops.example.com/callback\"\n ],\n \"scopes\": [\n \"shifts:read\",\n \"staff:read\"\n ],\n \"isPublic\": true\n}"
response = http.request(request)
puts response.read_body{
"id": 123,
"clientId": "<string>",
"clientName": "<string>",
"redirectUris": [
"<string>"
],
"isPublic": true,
"scopes": [
"<string>"
],
"metadata": {},
"registeredById": 123,
"registeredByName": "<string>",
"registrationSource": "dashboard",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"clientSecret": "<string>"
}{
"error": {
"message": "<string>",
"code": "<string>",
"details": {},
"suggestion": "<string>",
"retryable": true
},
"requestId": "<string>"
}{
"error": {
"message": "Invalid or revoked API key",
"code": "UNAUTHENTICATED",
"suggestion": "Send a valid `Authorization: Bearer <token>` (nex_live_/nex_pat_ key, JWT, or OAuth access token).",
"retryable": false
}
}{
"error": {
"message": "<string>",
"code": "<string>",
"details": {},
"suggestion": "<string>",
"retryable": true
},
"requestId": "<string>"
}Authorizations
JWT token authentication
Headers
Pin the request to a dated API version (e.g. 2026-05-10). Applied by app-level middleware to every /api route (server/routes/index.ts → apiVersionMiddleware), which echoes the resolved value back in the NexSpace-Version response header. Omit to get the latest version. Discover the catalog at GET /.well-known/api-versions.
^\d{4}-\d{2}-\d{2}$Replay guard for write requests. Applied by app-level middleware to every /api route (server/routes/index.ts → idempotencyMiddleware), which only acts on POST/PUT/PATCH — GET, DELETE and OPTIONS ignore the header. Re-sending the same key with an identical body within 24 hours replays the original response; the same key with a different body returns 409.
255Body
1 - 1201Requested scopes. Filtered to the headless catalog (shared/headless-scopes.ts) before storage.
1true (default) mints a public PKCE client with no secret. false mints a confidential client and returns a one-time clientSecret.
Response
Client registered
A registered OAuth client, as returned by the dashboard-managed /api/oauth-clients endpoints. The client secret is never included — only its one-time value on creation carries it.
Row id, used in DELETE /api/oauth-clients/{id}.
Public client identifier (cli_…).
Public (PKCE, no secret) vs confidential (secret-bearing).
Granted scopes, already filtered to the headless catalog.
client_uri / logo_uri as submitted.
The operator who registered it; null for RFC 7591 self-registration.
dashboard when registeredById is set (created via POST /api/oauth-clients), dynamic when the client self-registered via POST /oauth/register.
dashboard, dynamic Confidential clients only. Shown exactly once — it is stored hashed and can never be read back.

