Submit and Verify the Wallet Presentation
After the user approves the request, Credential Wallet signs a Verifiable Presentation and submits it to the submissionUrl embedded in the request. Your backend must verify the submission against the request state stored in document 06.
Submit the Credential from Credential Wallet
The wallet sends one JSON object to your submission endpoint:
{
"vpJwt": "<signed verifiable presentation JWT>",
"presentationSubmission": {
"id": "<submission id>",
"definition_id": "<presentation definition id>",
"descriptor_map": []
},
"pdRequestId": "<session id>",
"pdRequestType": "ForumAccountRequestVerifiableCredential",
"pdHash": "<presentation definition hash>",
"nonce": "<session nonce>",
"appId": "<registered app id>"
}
presentationSubmission is the camelCase outer field used by the current wallet and SDK. The SDK temporarily accepts the deprecated outer presentation_submission alias for compatibility, but new integrations should use presentationSubmission.
The frontend does not forward or transform this payload. The wallet posts directly to the backend submissionUrl.
Verify Wallet Submissions
Load the session before calling the SDK. Do not trust pdRequestId, pdHash, nonce, appId, or pdRequestType only because they arrived in the request body; compare them with the stored request.
app.post('/api/personhood/sessions/:sessionId/submit', async (req, res) => {
const session = await sessions.get(req.params.sessionId);
if (!session || session.status !== 'pending') {
return res.status(404).json({ error: 'SESSION_NOT_FOUND' });
}
if (new Date(session.expiresAt).getTime() <= Date.now()) {
await sessions.update(session.sessionId, { status: 'expired' });
return res.status(410).json({ error: 'SESSION_EXPIRED' });
}
try {
const verified = await presentation.verifySubmission({
submission: req.body,
expected: {
pdRequestId: session.sessionId,
pdRequestType: session.requestType,
pdHash: session.requestEnvelope.pdHash,
nonce: session.nonce,
appId: presentation.appConfig.appId,
subject: session.subject,
submissionUrl: session.submissionUrl,
targetCredentialType: session.verificationContext.targetCredentialType,
},
storedPresentationDefinition: session.presentationDefinition,
policy: session.verificationContext.policy,
});
return res.json({ ok: true, status: 'verified' });
} catch (error) {
await sessions.update(session.sessionId, {
status: 'rejected',
rejectedAt: new Date(),
});
return res.status(400).json({ ok: false, error: 'INVALID_PRESENTATION' });
}
});
What the SDK Verifies
verifySubmission validates the stored request and the wallet response together. It checks the request bindings, recomputes the stored Presentation Definition hash, validates the definition against app config, verifies the VP signature, evaluates Presentation Exchange, checks credential issuer and status, and confirms that the credential type satisfies the requested target type and policy.
The result is normalized for persistence:
{
holderDid,
walletDid,
issuerDid,
credentialJwt,
credentialTypes,
credentialTier,
credentialSubject,
issuanceDate,
expirationDate,
statusList,
normalized: {
name,
profilePicture,
profileUrl,
socialMedia,
nationality,
},
vpDigest,
}
If verification fails, do not grant access or use any credential attributes. Mark the session rejected or expired and return a generic error to the client. Avoid exposing cryptographic or policy-validation details in the browser.
After verification succeeds, your application decides what to authorize. It can complete the pending action immediately or let the frontend poll a session-status endpoint and retry the protected action.
Requested User Attributes
Requested attributes are available in verified.normalized. For example, if you requested name, profilePicture, socialMedia, and nationality:
const { name, profilePicture, socialMedia, nationality } = verified.normalized;
profileUrl is not currently supported.
Next Step
If you store a credential JWT for later authorization decisions, read Check the State of a Stored Personhood Credential.