-
Notifications
You must be signed in to change notification settings - Fork 0
CV Capture
Build an employer-facing experience with the useful parts of VerifyMyCreds—a candidate shares wallet credentials and the recruiter receives a readable CV—but without a public presentation URL.
The result should have these properties:
- The candidate knowingly shares with a named employer and for a named application purpose.
- The candidate link is submit-only. Possessing it must not grant access to the submitted CV.
- Only an authenticated recruiter who belongs to the target employer and is authorized for the application can read the result.
- The Credentialing Hub Operator credential never reaches a browser, mobile app, URL, log, or analytics event.
- The presentation is correlated to an opaque application ID, not to candidate PII.
- Access, verification, retention, and deletion are auditable.
- The employer is onboarded to Velocity Network and ia pproved to have "a Relying Party Service"
- The recruiter's employer is signed up to the platform.
- A tenant and relyingPartyService are provisioned on the CIH configured to receive all relevant credential types
- The recruiter signs in to the employer platform using the employer's SSO.
- On a specific application, the recruiter selects Request verified CV and enters the user's email address.
- The platform creates an application-scoped Credentialing Hub depot and a short-lived presentation link.
- The platform sends the candidate a QR code or deep link by email on behalf of the recruiter.
- The wallet shows the verified employer identity, the role/application purpose, requested credential categories, and retention period.
- The candidate selects credentials and consents.
- The wallet submits the presentation through the Credentialing Hub holder protocol. It does not submit to the recruiter's browser.
- The employer backend receives a signed notification or polls the Credentialing Hub, retrieves the presentation server-side, verifies it, and attaches it to the application.
- The recruiter sees a CV view with credential type, issuer, relevant dates, and verification state.

Figure 2 — VerifyMyCreds' current shared-CV summary: applicant identity, credential cards, issuer context, and the verification action. The employer implementation should preserve this readable recruiter experience while placing the route behind employer authorization.
- In the design showed above The user may tap verify to use a voucher and verify the CV. It verifies all credentials in a single request but that will tkae 5-20s depending on the number of credentials
- Recommended: Instead of user-requested verification, have the platform monitor submission and automatically verify on behalf of the recruiter. That means the button and processing can be removed.

Figure 3 — VerifyMyCreds after its own signed-in verification completed. The report is timestamped and both synthetic credentials are marked Verified. This is separate from the Credentialing Hub Operator verification request under the MDC Test tenant, which returned 402 before paid verification began.
- It is possible for one more checks to fail.
- Any one check failure should mark the entire credential as bad.

Figure 4 — A page from the exact VerifyMyCreds report data and PDF component after the one-line local radius workaround. It shows the employment credential and five successful checks.
sequenceDiagram
autonumber
actor R as Recruiter (person)
participant B as Employer application backend
participant W as Candidate wallet
participant H as CH holder protocol
participant O as CH Operator API
R->>B: Request verified CV for an application
B->>O: Create application-scoped depot
B->>O: Create or refresh presentation link
O-->>B: Return candidate-facing link
B-->>R: Display QR code or share link
R-->>W: Send link to candidate
W->>H: Fetch and verify signed request
H-->>W: Return signed presentation request
W->>H: Submit signed presentation after consent
H->>O: Store presentation in application depot
O-->>B: Notify or expose polling result
B->>O: Retrieve presentation by application depot
O-->>B: Return signed JWT_VP
B->>O: Verify presentation and credentials
O-->>B: Return verification results
B-->>R: Show private CV and verification states
Here, recruiter means a person using the employer application. Authenticated means the employer application has established that person's identity, current employer membership, and browser session, normally through SSO. Authentication alone is not authorization: the employer backend must still check that the recruiter may read the specific application on every request.
The browser speaks only to the employer backend. The backend owns the Credentialing Hub Operator credential and derives the permitted tenantId, serviceId, and depotId from the recruiter session and application. It must never accept those authorization boundaries directly from browser input.
For this version, “only the employer” means only recruiters authorized by the selected employer can use the product to read the captured CV. A public user, a holder of the candidate submission link, or a recruiter from another employer must not be able to read it.
This is achievable with the current APIs if all of the following are enforced:
- one Credentialing Hub tenant per employer;
- a backend-only Operator integration;
- recruiter SSO and employer membership checks;
- application-level authorization before every read;
- an application-scoped depot;
- row-level storage keyed by
employerIdandapplicationId; - employer-scoped encryption keys; and
- no public presentation route.
The platform operator and Credentialing Hub operator are trusted operational parties in this version and can access the data under administrative, audit, and data-processing controls. This is selected-employer isolation at the product boundary, not cryptographic exclusion of platform operators. State that limitation plainly in product, security, and contractual claims.
Use one tenant per legal employer. The tenant's DID document should register:
- the Credentialing Hub operator service;
- a career issuer service if this tenant will issue test credentials; and
- an inspector/relying-party service for CV capture.
The candidate wallet uses that DID document to verify who is requesting the presentation.
The live flow used POST /operator/relying-party-services/create with a single-use service:
{
"tenantId": "<employer-tenant-id>",
"service": {
"mode": "single",
"velocityNetworkServiceId": "#vlc-inspector-v-1-1",
"termsUrl": "https://employer.example/legal/candidate-privacy",
"description": "Verified CV for Software Engineer applications",
"disclosureRequest": {
"types": [
{ "type": "EmploymentCurrentV1.1" },
{ "type": "EmploymentPastV1.1" },
{ "type": "EducationDegreeGraduationV1.1" },
{ "type": "CertificationV1.1" },
{ "type": "CourseCompletionV1.1" }
],
"purpose": "Evaluate the candidate for the Software Engineer role",
"retentionPeriod": "P30D" // use a ISO8601 duration
},
"presentationRequestsExpireIn": 900
}
}Create exactly one relying-party depot for each sharing attempt:
POST /operator/depots/create
Authorization: Bearer <operator-token>
Content-Type: application/json{
"tenantId": "<employer-tenant-id>",
"serviceId": "<cv-relying-party-service-id>",
"depot": {
"userReference": "<opaque-application-reference>"
}
}Generate userReference with a keyed digest or random lookup ID. Do not put a name, email address, job title, ATS URL, or CV content in it.
For example:
const userReference = hmacSha256(
applicationReferenceKey,
`${employerId}:${applicationId}:${shareAttemptId}`,
);Store this mapping in the employer database:
employerId + applicationId + shareAttemptId
-> tenantId + serviceId + depotId + userReference
POST /operator/presentation-links/refresh
Authorization: Bearer <operator-token>
Content-Type: application/json{
"tenantId": "<employer-tenant-id>",
"serviceId": "<cv-relying-party-service-id>",
"depotId": "<application-depot-id>"
}The response includes a Velocity Network deep link, an OpenID4VP link, and a browser redirect URL. Return only the candidate-facing link to the recruiter browser. Never return the Operator credential or the Operator retrieval URL.
Treat the link as short-lived and single-purpose. Forwarding it can let another wallet submit data, but it must not let the holder read an already submitted CV.
The live application correlation worked only when the custom wallet reproduced the SDK's deep-link expansion.
The outer link contains fields similar to:
<wallet-inspect-scheme>://inspect?
request_uri=https%3A%2F%2F...%2Fget-presentation-request%3Fid%3D...
&inspectorDid=did%3A...
&vendorOriginContext=depot%3A...
The wallet must:
- Read
request_uri. - Append every other non-empty outer query parameter to that request URI.
- Fetch the expanded request URI with the Velocity protocol version header.
- Verify the signed presentation request and the requesting employer DID.
- Display employer identity, purpose, credential request, terms, and retention.
- Echo
vendorOriginContextinside the submitted VP.
Conceptually:
const deepLink = new URL(link);
const requestUrl = new URL(deepLink.searchParams.get('request_uri')!);
for (const [key, value] of deepLink.searchParams) {
if (key !== 'request_uri' && value !== '') {
requestUrl.searchParams.set(key, value);
}
}
const request = await getSignedPresentationRequest(requestUrl);
verifyPresentationRequest(request, request.metadata.client_name);The signed VP should have the same material shape produced by the wallet SDK:
{
"iss": "did:jwk:<wallet-public-key>",
"jti": "<unique-presentation-id>",
"vp": {
"@context": ["https://www.w3.org/2018/credentials/v1"],
"type": "VerifiablePresentation",
"presentation_submission": {
"id": "<unique-submission-id>",
"definition_id": "<definition-id-from-request>",
"descriptor_map": [
{
"id": "EmploymentCurrentV1.1",
"format": "jwt_vc",
"path": "$.verifiableCredential[0]"
}
]
},
"verifiableCredential": ["<signed-jwt-vc>"],
"vendorOriginContext": "<value-from-outer-link>"
}
}Submit it to metadata.submit_presentation_uri as:
{
"exchange_id": "<exchange-id-from-request>",
"jwt_vp": "<signed-jwt-vp>"
}In the live test, omitting the outer-parameter expansion caused the Credentialing Hub to create a new uncorrelated depot. Echoing the context only in the later VP was insufficient because the exchange had already been created by the presentation-request call.
For a production wallet, prefer the supplied wallet SDK or OpenID4VP implementation over handwritten signing code. The handwritten simulation here was used only to validate the protocol without a human mobile-wallet step.
Use a signed Credentialing Hub event if notification delivery is configured. Verify the event signature over the raw request body, deduplicate by event ID, and treat the event as a cue to fetch—not as the CV itself.
Polling fallback:
GET /operator/presentations/get?tenantId=<tenant>&depotId=<application-depot>
Authorization: Bearer <operator-token>The live wallet-exact test returned one presentation whose depotId exactly matched the application depot.
Before returning anything to the recruiter browser, the employer backend must check:
session.user is active
AND session.employerId == application.employerId
AND recruiter has permission to read this application
AND application.tenantId == configured tenant for session.employerId
AND presentation.depotId == application.currentShareDepotId
Do not accept tenantId, depotId, presentationId, or employerId from the browser as an authorization decision. The browser may name an applicationId; the backend must derive everything else.
Call:
POST /operator/presentations/verify
Authorization: Bearer <operator-token>
Content-Type: application/json{
"tenantId": "<employer-tenant-id>",
"presentationId": "<captured-presentation-id>"
}Display separate states rather than one ambiguous badge:
- presentation holder signature;
- credential tamper check;
- trusted issuer check;
- trusted holder check;
- revocation check;
- expiry check; and
- verification timestamp.
The Credentialing Hub Operator verification call under the MDC Test tenant returned 402 verification_payment_required because that tenant had exhausted its vouchers. The response occurred before paid verification began. The presentation was captured successfully and its wallet signature was independently verified, but that run must not be represented as completed Credentialing Hub verification.

Figure 5 — VerifyMyCreds' employment-detail view. The private employer version should retain the credential facts and issuer context, then add explicit holder-signature, integrity, issuer-trust, revocation, expiry, and verification-time states.

Figure 6 — VerifyMyCreds' education-detail view. Credential details remain evidence attached to the selected application; they are not a public profile.
This is useful for integration tests and demos; it is not part of the recruiter production path.
- Use an issuer service configured for internal pre-authorization.
- Create an issuer depot with an opaque synthetic wallet reference.
- Add synthetic credential offers with
POST /operator/credentials/create. - Generate the deep links with
POST /operator/issue-links/refresh. - Have the wallet fetch and verify the signed credential manifest from
request_uri. - Authenticate by submitting a holder-signed VP containing the link's
vendorOriginContext. - Use the returned exchange token to fetch the credential offers.
- Approve offer IDs and answer the issuing challenge with a holder proof JWT.
- Store the returned signed JWT VCs in the wallet.
The live test completed this sequence and received two JWT VCs.
One test-environment limitation surfaced: adding EmailV1.0 to the issuance depot caused /operator/issue-links/refresh to fail because foundation.velocitynetwork.EmailV1.0 was absent from credential_configurations_supported. The endpoint generated both VN and OpenID links, so the missing OpenID configuration blocked the whole refresh. Employment and education credentials issued successfully after the synthetic email offer was removed.
- Encrypt stored presentations with a separate key per employer.
- Keep raw JWTs out of logs, traces, error reports, analytics, and support tooling.
- Record recruiter reads with employer ID, application ID, user ID, time, purpose, and result.
- Render only the credential fields required for recruitment.
- Set the candidate-visible retention period to the actual backend deletion schedule.
- Expire unused presentation links quickly and invalidate a share attempt after completion.
- Prevent recruiters from searching by raw depot ID or presentation ID.
- Apply rate limits to link creation, presentation polling, and recruiter reads.
- Make a recruiter leaving the employer immediately lose all access.
- Define subject-access, correction, withdrawal, and deletion procedures.
The current Operator documentation exposes credential and depot deletion, but no presentation deletion endpoint was found. Do not promise automated raw-presentation deletion until there is a supported purge mechanism and an acceptance test that proves it.
- An authorized recruiter creates one request for one application.
- A wallet verifies the employer DID and request signature.
- The wallet can select and submit compatible credentials.
- The resulting presentation is attached to the expected application depot.
- The recruiter sees the candidate evidence and exact verification states.
- An unauthenticated user receives
401or404, never presentation data. - A recruiter from employer B cannot access employer A's application by changing route IDs.
- A recruiter from employer A without application permission cannot access it.
- Browser-supplied
tenantId,depotId, andpresentationIdare ignored for authorization. - Forwarding the candidate share link does not expose an existing presentation.
- The Operator token is absent from page source, browser storage, network responses, and telemetry.
- The wallet rejects an invalid employer request signature.
- The wallet forwards the outer deep-link parameters to
request_uri. - The wallet echoes
vendorOriginContextin the VP. - The descriptor map matches the selected credential types and array paths.
- Replaying a completed exchange is rejected or produces no duplicate application record.
- Voucher exhaustion produces a clear “verification unavailable” state, not a green badge.
- Revoked, expired, untrusted, and tampered credentials are shown distinctly.
- A PDF renderer error produces a visible failure state; print is disabled and download never appears as an active link with an empty
href. - A successful PDF build produces a non-empty download and a report whose verification checks match the on-screen result.
- A notification retry is idempotent.
- An expired request can be refreshed without changing the application authorization boundary.
- Deletion removes the recruiter view, derived data, raw presentation, caches, and backups according to policy.