Documentation Index
Fetch the complete documentation index at: https://docs.moca.network/llms.txt
Use this file to discover all available pages before exploring further.
This recipe shows how to issue a verifiable KYC credential the moment your identity provider confirms a user, using the Issue on Behalf API. The user never needs to be in an active session.
What you’ll build
- A webhook handler that fires when your KYC provider confirms a user.
- A Partner JWT signed with the
issue scope targeting that user’s email.
- An Issue on Behalf API call that queues the credential for on-chain issuance.
- A status poller that confirms the credential is
ONCHAIN.
Prerequisites
- Issue on Behalf enabled for your partner account
- A published issuance program with a schema that includes KYC fields (e.g.
kycVerified, kycLevel, verifiedAt)
- Partner JWT signing configured (RS256 or ES256)
Step 1: Handle the KYC webhook
When your KYC provider (Sumsub, Onfido, Jumio, etc.) sends a verification-complete callback, extract the user email and verification result.
const express = require("express");
const app = express();
app.use(express.json());
app.post("/webhooks/kyc-complete", async (req, res) => {
const { userEmail, kycLevel, verifiedAt } = req.body;
if (!userEmail) return res.status(400).json({ error: "Missing userEmail" });
try {
const result = await issueKycCredential(userEmail, { kycLevel, verifiedAt });
res.json({ success: true, coreClaimHash: result.coreClaimHash });
} catch (err) {
console.error("Issue on Behalf failed:", err.message);
res.status(500).json({ error: err.message });
}
});
Step 2: Sign a Partner JWT
Generate a short-lived JWT with scope: "issue" and the target user’s email.
const jwt = require("jsonwebtoken");
const fs = require("fs");
const privateKey = fs.readFileSync("path/to/private.key");
function getPartnerJwt(email) {
const now = Math.floor(Date.now() / 1000);
return jwt.sign(
{
partnerId: process.env.PARTNER_ID,
scope: "issue",
email,
iat: now,
exp: now + 5 * 60,
},
privateKey,
{
algorithm: "RS256",
header: { kid: process.env.KEY_ID, typ: "JWT" },
}
);
}
Step 3: Call Issue on Behalf
const BASE_URL =
process.env.NODE_ENV === "production"
? "https://api.mocachain.org/v1"
: "https://api.sandbox.mocachain.org/v1";
async function issueKycCredential(userEmail, kycData) {
const token = getPartnerJwt(userEmail);
const res = await fetch(`${BASE_URL}/credentials/issue-on-behalf`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-partner-auth": token,
},
body: JSON.stringify({
issuerDid: process.env.ISSUER_DID,
credentialId: process.env.CREDENTIAL_ID,
credentialSubject: {
kycVerified: true,
kycLevel: kycData.kycLevel,
verifiedAt: kycData.verifiedAt || new Date().toISOString(),
},
onDuplicate: "revoke",
}),
});
if (!res.ok) throw new Error(`Issue failed: ${res.status} ${await res.text()}`);
return res.json();
}
Step 4: Poll for on-chain confirmation
Issuance is asynchronous. Poll the status endpoint until vcStatus is ONCHAIN.
async function waitForOnchain(userEmail, coreClaimHash, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
const token = getPartnerJwt(userEmail);
const res = await fetch(
`${BASE_URL}/credentials/status?coreClaimHash=${encodeURIComponent(coreClaimHash)}`,
{ headers: { "x-partner-auth": token } }
);
const data = await res.json();
if (data.vcStatus === "ONCHAIN") return data;
await new Promise((r) => setTimeout(r, Math.pow(2, i) * 1000));
}
throw new Error("Timed out waiting for ONCHAIN status");
}
Full flow
app.post("/webhooks/kyc-complete", async (req, res) => {
const { userEmail, kycLevel } = req.body;
const issued = await issueKycCredential(userEmail, {
kycLevel,
verifiedAt: new Date().toISOString(),
});
const confirmed = await waitForOnchain(userEmail, issued.coreClaimHash);
console.log(`Credential ${confirmed.vcId} is on-chain for ${userEmail}`);
res.json({ success: true, vcId: confirmed.vcId });
});
Next steps