curl -sS -X POST "https://phosra-api-sandbox-production.up.railway.app/api/v1/advisors/self-register" \
-H "Content-Type: application/json" \
-d '{
"did": "did:ocss:acme-advisor",
"public_key_b64url": "REPLACE_WITH_YOUR_ED25519_PUBLIC_KEY_B64URL",
"roles": [
"advising-agency"
],
"entry_role": "verifying-agency"
}'const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
const res = await fetch(`${BASE}/advisors/self-register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"did": "did:ocss:acme-advisor",
"public_key_b64url": "REPLACE_WITH_YOUR_ED25519_PUBLIC_KEY_B64URL",
"roles": [
"advising-agency"
],
"entry_role": "verifying-agency"
}),
});
console.log(res.status, await res.json());
import requests
BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
res = requests.post(
f"{BASE}/advisors/self-register",
json={
"did": "did:ocss:acme-advisor",
"public_key_b64url": "REPLACE_WITH_YOUR_ED25519_PUBLIC_KEY_B64URL",
"roles": [
"advising-agency"
],
"entry_role": "verifying-agency"
},
)
print(res.status_code, res.json())
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
body := bytes.NewBufferString(`{
"did": "did:ocss:acme-advisor",
"public_key_b64url": "REPLACE_WITH_YOUR_ED25519_PUBLIC_KEY_B64URL",
"roles": [
"advising-agency"
],
"entry_role": "verifying-agency"
}`)
req, _ := http.NewRequest("POST", base+"/advisors/self-register", body)
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.Status, string(out))
}
<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://phosra-api-sandbox-production.up.railway.app/api/v1/advisors/self-register",
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([
'did' => 'did:ocss:touchstone',
'public_key_b64url' => 'CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU',
'roles' => [
'<string>'
],
'entry_role' => 'verifying-agency'
]),
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;
}HttpResponse<String> response = Unirest.post("https://phosra-api-sandbox-production.up.railway.app/api/v1/advisors/self-register")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"did\": \"did:ocss:touchstone\",\n \"public_key_b64url\": \"CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU\",\n \"roles\": [\n \"<string>\"\n ],\n \"entry_role\": \"verifying-agency\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://phosra-api-sandbox-production.up.railway.app/api/v1/advisors/self-register")
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 \"did\": \"did:ocss:touchstone\",\n \"public_key_b64url\": \"CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU\",\n \"roles\": [\n \"<string>\"\n ],\n \"entry_role\": \"verifying-agency\"\n}"
response = http.request(request)
puts response.read_body{
"advisor_id": "7e4c1a90-3b28-4d67-95f1-0a8c6b2e9d43",
"did": "did:ocss:touchstone",
"key_id": "did:ocss:touchstone#2026-07",
"kid": "2026-07",
"published_key_x": "CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU",
"trust_tier": "provisional",
"entry_role": "verifying-agency"
}{
"error": "Bad Request",
"message": "public_key_b64url is required",
"code": 400
}{
"error": "Unauthorized",
"message": "missing authorization header",
"code": 401
}{
"error": "Forbidden",
"message": "self_register_sandbox_only: self-service DID registration is only permitted in sandbox environments (SANDBOX_MODE=true)",
"code": 403
}{
"error": "Conflict",
"message": "did_already_registered: this DID is already on the trust list; self-register cannot overwrite existing entries",
"code": 409
}{
"error": "Too Many Requests",
"message": "rate limit exceeded",
"code": 429
}{
"error": "Internal Server Error",
"message": "internal error",
"code": 500
}{
"error": "Bad Gateway",
"message": "downstream provider error",
"code": 502
}{
"error": "Service Unavailable",
"message": "downstream provider unavailable",
"code": 503
}Self-register advisor (sandbox)
Sandbox-only self-service onboarding. Inserts a did:ocss:<slug> entry at provisional tier from a raw Ed25519 public key, then recompiles the served Trust List so the DID appears immediately. Requires SANDBOX_MODE=true on the census — production returns 403 self_register_sandbox_only. Cannot overwrite an existing entry (409) and never escalates above provisional. No auth is required (the sandbox guard is the gate).
curl -sS -X POST "https://phosra-api-sandbox-production.up.railway.app/api/v1/advisors/self-register" \
-H "Content-Type: application/json" \
-d '{
"did": "did:ocss:acme-advisor",
"public_key_b64url": "REPLACE_WITH_YOUR_ED25519_PUBLIC_KEY_B64URL",
"roles": [
"advising-agency"
],
"entry_role": "verifying-agency"
}'const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
const res = await fetch(`${BASE}/advisors/self-register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"did": "did:ocss:acme-advisor",
"public_key_b64url": "REPLACE_WITH_YOUR_ED25519_PUBLIC_KEY_B64URL",
"roles": [
"advising-agency"
],
"entry_role": "verifying-agency"
}),
});
console.log(res.status, await res.json());
import requests
BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
res = requests.post(
f"{BASE}/advisors/self-register",
json={
"did": "did:ocss:acme-advisor",
"public_key_b64url": "REPLACE_WITH_YOUR_ED25519_PUBLIC_KEY_B64URL",
"roles": [
"advising-agency"
],
"entry_role": "verifying-agency"
},
)
print(res.status_code, res.json())
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
body := bytes.NewBufferString(`{
"did": "did:ocss:acme-advisor",
"public_key_b64url": "REPLACE_WITH_YOUR_ED25519_PUBLIC_KEY_B64URL",
"roles": [
"advising-agency"
],
"entry_role": "verifying-agency"
}`)
req, _ := http.NewRequest("POST", base+"/advisors/self-register", body)
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.Status, string(out))
}
<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://phosra-api-sandbox-production.up.railway.app/api/v1/advisors/self-register",
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([
'did' => 'did:ocss:touchstone',
'public_key_b64url' => 'CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU',
'roles' => [
'<string>'
],
'entry_role' => 'verifying-agency'
]),
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;
}HttpResponse<String> response = Unirest.post("https://phosra-api-sandbox-production.up.railway.app/api/v1/advisors/self-register")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"did\": \"did:ocss:touchstone\",\n \"public_key_b64url\": \"CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU\",\n \"roles\": [\n \"<string>\"\n ],\n \"entry_role\": \"verifying-agency\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://phosra-api-sandbox-production.up.railway.app/api/v1/advisors/self-register")
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 \"did\": \"did:ocss:touchstone\",\n \"public_key_b64url\": \"CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU\",\n \"roles\": [\n \"<string>\"\n ],\n \"entry_role\": \"verifying-agency\"\n}"
response = http.request(request)
puts response.read_body{
"advisor_id": "7e4c1a90-3b28-4d67-95f1-0a8c6b2e9d43",
"did": "did:ocss:touchstone",
"key_id": "did:ocss:touchstone#2026-07",
"kid": "2026-07",
"published_key_x": "CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU",
"trust_tier": "provisional",
"entry_role": "verifying-agency"
}{
"error": "Bad Request",
"message": "public_key_b64url is required",
"code": 400
}{
"error": "Unauthorized",
"message": "missing authorization header",
"code": 401
}{
"error": "Forbidden",
"message": "self_register_sandbox_only: self-service DID registration is only permitted in sandbox environments (SANDBOX_MODE=true)",
"code": 403
}{
"error": "Conflict",
"message": "did_already_registered: this DID is already on the trust list; self-register cannot overwrite existing entries",
"code": 409
}{
"error": "Too Many Requests",
"message": "rate limit exceeded",
"code": 429
}{
"error": "Internal Server Error",
"message": "internal error",
"code": 500
}{
"error": "Bad Gateway",
"message": "downstream provider error",
"code": 502
}{
"error": "Service Unavailable",
"message": "downstream provider unavailable",
"code": 503
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
The DID to register, did:ocss:<slug>.
"did:ocss:touchstone"
Raw 32-byte Ed25519 public key, base64url unpadded (RFC 4648 §5, no =). The census PKIX-wraps it for the Trust List.
"CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU"
Informational role tags, stored verbatim on the provisional entry.
Optional Trust-List role marker. Only verifying-agency (§5.4 open assessor market) is accepted; any other value returns 400.
verifying-agency Response
DID registered at provisional tier and published to the Trust List.
UUID of the advisor row created for this DID (returned first in the live wire body).
Your full signing key id, did:ocss:<slug>#<kid> — use verbatim as SenderKey.keyID. The census now returns this directly in the 200 body.
"did:ocss:touchstone#2026-07"
The bare kid the census bound — the current UTC month (YYYY-MM) at registration time.
"2026-07"
The base64url public key now published on the Trust List entry.
Always provisional — self-service never elevates the tier.
Echoed only when an entry_role was requested.