Skip to content

Milestone: M5 — Disclosure and Verification (also closes a gap in M3's issuer onboarding) | Requirement Clarity: ⚪ Out of scope — confirmed with client 2026-09-15. Not being built. Manual dashboard registration (already the working, proven path for every org onboarded so far) remains the process going forward. | Dev Status: ⚪ Won't implement — closed by client decision, not a technical blocker anymore

2026-09-15 update — confirmed out of scope, closing this out

Client confirmed this is out of scope for MVP — the manual per-org dashboard registration is accepted as the ongoing process, not something to automate right now. This resolves the investigation below without needing Velocity's answer on the client-credentials OAuth question; that thread can be dropped unless scope changes later.

Kept everything below for reference, in case this gets revisited post-MVP — the technical investigation (real 404 on the wrong HTTP method, the actual live endpoints, the OAuth scope requirements) stays accurate if this is picked up again.

2026-09-10 update — implementation blocked, not just under-specified

Attempted to verify the two endpoints below live before writing any code (per this doc's own "confirm before building" advice). Both fail identically on both reachable backends:

GET https://devregistrar.velocitynetwork.foundation/api/v0.6/organizations/{did}/services      → 404
GET https://stagingregistrar.velocitynetwork.foundation/api/v0.6/organizations/{did}/services   → 404
Response body: {"message":"Route GET:/api/v0.6/organizations/.../services not found","error":"Not Found","statusCode":404} — a route-level 404 from the server itself (NestJS's literal "route not found" shape), not a business-logic error. For comparison, verified-profile on the exact same base URL and DID (already used elsewhere in this codebase) returns a clean 200 — so connectivity, DID format, and general reachability aren't the problem. The /services and /activate-services routes are documented on docs.velocitynetwork.foundation but genuinely not deployed on either registrar backend we have credentials for.

This means the Registrar dashboard screenshot that prompted this doc is almost certainly not calling this public bearer-token API at all — more likely a session/cookie-authenticated internal admin surface (possibly under stagingregistrarapp.velocitynetwork.foundation, the separate domain the dashboard itself lives on), which our backend has no access path to today.

Do not implement the client code in "Suggested build order" below until Velocity confirms this API is actually live somewhere we can reach. Treat this the same way as M5-11's credits/vouchers mechanism — a real requirement with no confirmed, callable implementation yet. Folded into the same outreach thread already open with Velocity's team (the widget bug + voucher balance emails) — ask whether these endpoints exist anywhere reachable, and if the dashboard uses a different API we'd need separate access to call.

The one-time backfill for existing dev/UAT orgs is blocked for the identical reason — there's no reachable API call to backfill with. Until this is unblocked, any org missing a DID service (Career Issuer, Credential Agent Operator, or Inspector) needs the same manual dashboard fix already done for TESCO LLC's Inspector service and the org that surfaced this whole issue — one org at a time, by hand, through Velocity's Registrar dashboard.

What's usable right now, while this is blocked

The manual path isn't a dead end — it's the only path today, and it works:

  1. Per-org fix: log into Velocity's Registrar dashboard for the affected org, add the missing service (Career Issuer / Credential Agent Operator / Inspector as needed), then activate it — exactly what was already done for the org that surfaced this issue. No need to wait on Velocity's answer to do this for any other org that needs it right now.
  2. Audit which orgs are missing what: GET /organizations/{did}/services is a plain unauthenticated read — worth running against every known org (local, dev, UAT) to build an accurate list of who's missing which service, so the manual fixes can be done in one pass rather than discovered one at a time when a candidate hits an error. This part can be scripted today even though add/activate can't.
  3. Once Velocity confirms a usable OAuth grant: come back to the "Suggested build order" above and implement the automated version — nothing about the code plan changes, only the auth wiring.

M5-18 — Automate Registrar DID service registration on org onboarding

Why this exists

Found 2026-09-10 while disclosure-testing a second organization: the deep link errored for the candidate until the org's Relying Party (VlcInspector_v1) DID service was added and activated manually through Velocity's Registrar dashboard. TESCO LLC (our main test org) had never hit this because someone had already added it manually at some earlier point. Every other org we create will hit the same wall until this is automated.

Investigating further, this isn't Inspector-specific — no DID service has ever been registered automatically for any org, for any of the three services our platform depends on (Career Issuer, Credential Agent Operator, Relying Party/Inspector). VelocityRegistrarService.registerOrganization() does have a serviceEndpoints field in its payload, but it's hardcoded to [] and, more importantly, that method is never called from the live org-creation path at all — organization.service.ts only calls updateOrganization/deleteOrganization on an org that already has a velocityRegistrarId. This matches a comment already in .env.development: "For the POC we register real orgs via the Velocity dashboard UI, not this API." Org + DID registration on Velocity has always been a manual step done through their dashboard, and apparently so has every DID service on top of it.

Decision: trigger point is "after issuer enable," not "org creation"

At the moment our platform's createOrganization() runs, the org doesn't have a Velocity DID yet — organizationDid is either typed in by hand as part of that same form, or set later via a distinct superadmin "enable issuer" action (superadmin-organization.service.ts:73). Either way, there's no guarantee a usable DID exists at org-creation time, so there's nothing to register a service against yet.

The one place in the code that's guaranteed to have a real, usable DID + tenant is OrganizationService.transitionToIssuerEnabled() (app/backend/src/admin/services/organization.service.ts:713) — it explicitly throws if organization.organizationDid is missing (line 739-743), then calls velocityTenantService.createTenant(), then sets issuerEnabledAt. By the time that method returns successfully, the DID exists on Velocity's side and the CIH tenant is live. That's the right hook for the two issuer-side services.

Inspector is a separate case: RelyingPartyOnboardingService.provisionForOrg() (app/backend/src/verification/services/relying-party-onboarding.service.ts:33) already requires an ACTIVE issuer tenant as a precondition (resolveTenantContext() throws otherwise) — so it always runs strictly after issuer enablement anyway. That's the natural, already-existing hook for the Inspector service.

Net result — two hook points, not one:

Service type Hook point Why
Career Issuer VlcCareerIssuer_v1 OrganizationService.transitionToIssuerEnabled(), right after velocityTenantService.createTenant() succeeds DID + tenant guaranteed to exist here
Credential Agent Operator VlcCredentialAgentOperator_v1 same as above same reason
Relying Party / Inspector VlcInspector_v1 RelyingPartyOnboardingService.provisionForOrg(), right after resolveTenantContext() succeeds, before the CIH createRelyingPartyService call already gated on issuer being enabled; this is where we first act as Inspector

Nothing about the org-creation flow itself needs to change.

The two Registrar API calls (confirmed against live docs, 2026-09-10)

1. Add a service

POST {registrarBaseUrl}{registrarApiPath}/organizations/{did}/services
Confirmed live at docs.velocitynetwork.foundation/docs/registrar-api/4cb6cefa16e5a-add-a-new-service.

Request body:

{
  "id": "#vlc-inspector-v-1-1",
  "type": "VlcInspector_v1",
  "serviceEndpoint": "did:web:stagingregistrar.velocitynetwork.foundation:d:<org-slug>#vlc-credential-agent-operator-v-1-1",
  "invitationCode": "<if the org was created via an invitation>"
}
- type is a closed enum: VlcCredentialAgentOperator_v1, VlcCareerIssuer_v1, VlcInspector_v1, plus wallet/notary/node types we don't use. - serviceEndpoint for Career Issuer and Inspector both point back at the org's own Credential Agent Operator service via a DID service-reference (did:web:...#vlc-credential-agent-operator-v-1-1) — exactly the pattern in the screenshot you pulled from the dashboard. Credential Agent Operator's own serviceEndpoint is our real HTTPS API base (https://<our-domain>/api/v1/velocity per the screenshot, though worth confirming this exact path against what VelocityTenantService already sends elsewhere). - Response: 201 with { "service": { "id", "type", "serviceEndpoint", ... } }.

2. Activate the service

POST {registrarBaseUrl}{registrarApiPath}/organizations/{did}/activate-services
Confirmed live at docs.velocitynetwork.foundation/docs/registrar-api/fa5a183dd6c16-activate-services.

Request body:

{ "serviceIds": ["<the id returned from step 1>"] }
Response: 200 with the org's full profile plus activatedSerivceIds (their typo, not ours) confirming what's now active. This is a required second step — a service added but not activated doesn't work, matching the "ACTIVATED" badge behavior in the dashboard screenshot.

Open questions to resolve before writing code

  1. Auth mechanism unconfirmed. The docs' example curl for both endpoints shows no Authorization header, but Stoplight examples routinely omit auth even when required. Our existing VelocityRegistrarService.getVerifiedProfile() uses Authorization: Bearer ${registrarJwt} against the Registrar — but VNF_REGISTRAR_JWT is empty in our local .env.development, so that path is untested locally. A mutating "add a service to my own DID document" call plausibly needs to be authenticated as that specific org (its own signing key), not a shared platform-wide token — needs verifying against real staging before assuming the existing registrarJwt pattern just works here.
  2. invitationCode requirement is unclear. The schema lists it as optional but describes it as "the invitationCode used to generate this organization" — if an org was registered without an invitation flow (e.g., typed in manually, which is how this has worked so far), it's not obvious what to send, or whether it's required in practice despite the schema not marking it so.
  3. Idempotency / find-or-create. Needs a check before adding — GET /organizations/{did}/services (present in the same nav, not yet explored in detail) to see if a service of this type already exists (e.g., an org where someone already added it manually mid-testing, like the one that prompted this doc) before calling add, mirroring the find-or-create pattern already used in RelyingPartyOnboardingService.provisionForOrg() and VelocityTenantService.createTenant().
  4. Exact serviceEndpoint values need to come from real config, not hardcoded. The screenshot's example used curo-rec.test — production/staging/dev/UAT will each need their own real domain substituted in, sourced from existing config (likely API_BASE_URL / FRONTEND_URL plus the org's own DID) rather than typed literally.

Suggested build order

  1. Confirm auth mechanism and invitationCode handling against real staging with a manual test call (Postman/curl) before writing any service code — cheap to verify, expensive to guess wrong.
  2. Add getServices, addService, activateService methods to VelocityRegistrarService (or a new focused service, matching this codebase's existing split between issuer/inspector API clients) — same typed request/response + retry/error-mapping pattern already used throughout.
  3. Wire Career Issuer + Credential Agent Operator registration into transitionToIssuerEnabled(), find-or-create, with the same "don't roll back what already succeeded" philosophy as RelyingPartyOnboardingService (tenant creation succeeding matters more than the service-registration follow-up failing).
  4. Wire Inspector registration into RelyingPartyOnboardingService.provisionForOrg() the same way.
  5. Backfill: manually verify (or re-run for) any already-existing orgs missing these services — TESCO LLC already has all three; the org that surfaced this bug now has Inspector added manually but should be checked for the other two as well.
  • M5-00 implementation guide — parent doc, disclosure/verification overview
  • app/backend/src/admin/services/organization.service.ts:713 — transitionToIssuerEnabled()
  • app/backend/src/admin/services/velocity-registrar.service.ts — existing Registrar client, serviceEndpoints: [] gap noted here
  • app/backend/src/verification/services/relying-party-onboarding.service.ts — existing Inspector CIH-side provisioning, second hook point