Integration Guide
Integrate Veil into your DeFi protocol in five steps: register as an issuer, submit behavioral events as users interact with your protocol, obtain a challenge, request a credit decision using the user's Veil DID, and act on the result.
Prerequisites
- Issuer registration: Your protocol must be approved by the Veil admin before you can submit events. Contact the Veil team to begin registration. You will receive an
issuerPk— keep this secure. - Veil API access: Set
VEIL_API_URLto the Veil backend URL provided during onboarding. Current preview endpoint:https://api.13-61-145-21.sslip.io/api/v1. - User Veil public key (
userPk): Users derive their Veil public key in the dashboard. Behavioral events are keyed to this value. Events submitted before a user creates their score profile are queued until they do. - User Veil DID and Identity Pass: For credit decisions, the user must have minted a Veil Identity Pass on CKB. This generates their
did:veil:…identifier and registers it on Midnight. The DID is the only piece of information you need to request a credit decision.
Step 1: Register as an Issuer
Issuer registration is a one-time process handled by the Veil admin. The admin callsAdmin_addIssuer on-chain with your protocol name and contract address. Upon registration you receive:
issuerPk— your protocol's identity key within Veil. Include this in every event submission.
Registered issuers are listed in the contract's public ledger state. Any event submitted with an unregistered issuer key is rejected by the ZK circuit.
issuerPkis a public key derived from your protocol's registration. While it is not a secret, it is your stable identity in the Veil system — store it securely in your backend configuration and do not rotate it without coordinating with the Veil admin.Step 2: Submit Behavioral Events
As users interact with your protocol, submit behavioral events in real time. Four event types are available — see the Scoring Model for the full field reference.
| User action | Event endpoint |
|---|---|
| Loan repaid (on time or late) | POST /scoring-events/repayments |
| Position liquidated | POST /scoring-events/liquidations |
| User interacted with your protocol | POST /scoring-events/protocol-usage |
| Periodic debt state snapshot | POST /scoring-events/debt-states |
const VEIL_API = process.env.VEIL_API_URL ?? 'https://api.13-61-145-21.sslip.io/api/v1';
// Submit a repayment event
const res = await fetch(`${VEIL_API}/scoring-events/repayments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userPk: '0xabc123...', // user's Veil public key (32 bytes hex)
issuerPk: '0xdef456...', // your registered issuer public key
paidOnTimeFlag: 1, // 1 = on time, 0 = late
amountWeight: 1000, // normalized loan size
eventEpoch: 3, // current protocol epoch
}),
});
const { job } = await res.json();
// → { id: "job_...", status: "pending" }All event endpoints return a job. Because events are processed via ZK circuits on Midnight, they are queued asynchronously. You do not need to wait for one event to complete before submitting the next — events are processed independently.
Step 3: Obtain a Challenge
Before requesting a credit decision, obtain a one-time challenge from the Veil backend. Challenges expire in 60 seconds and cannot be reused — they prevent replay attacks on the authorization flow.
// Request a one-time challenge (valid for 60 seconds)
const res = await fetch(`${VEIL_API}/challenges`, { method: 'POST' });
const { challenge, challengeExpiresAt } = await res.json();
// Use challenge in POST /credit-decisions within 60sPresent the challenge to the user so their CKB wallet can sign the decision authorization message (step 4).
Step 4: Request a Credit Decision
A credit decision verifies that the user holds a valid Veil Identity Pass on CKB, that their DID is registered on Midnight, and that they have authorized this specific request. The user signs a short message with their CKB wallet to prove they control their identity.
There are two ways to send a credit decision request — the DID flow is strongly recommended for all new integrations.
Using the DID Flow (Recommended)
The DID flow only requires the user's did:veil:…identifier. The backend resolves the DID internally to look up the user's identity pass, Midnight score, and CKB wallet — you do not need to pass userPk, veilIdHash, sporeId, or userCkbAddress separately.
// The recommended flow uses the user's Veil DID (did:veil:0x...)
// No need to pass userPk, veilIdHash, sporeId, or userCkbAddress separately.
// 1. Get the user's Veil DID — they share it or you resolve it from their sporeId.
const veilDid = 'did:veil:0x...'; // e.g. from the user's dashboard
// 2. Obtain a fresh challenge
const { challenge } = await fetch(`${VEIL_API}/challenges`, { method: 'POST' })
.then((r) => r.json());
// 3. Build the message the user's CKB wallet must sign
const verificationMethod = `${veilDid}#ckb-owner-1`;
const message = [
'Veil credit decision authorization',
`did:${veilDid}`,
`challenge:${challenge}`,
`verificationMethod:${verificationMethod}`,
'registryVersion:1',
'purpose:credit-decision',
].join('\n');
// 4. Have the user's CKB wallet sign the message (e.g. via CCC connector)
const authorization = await ckbSigner.signMessage(message);
// 5. Submit the credit decision request
const res = await fetch(`${VEIL_API}/credit-decisions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
did: veilDid, // ← the Veil DID
challenge,
authorization: {
...authorization,
verificationMethod, // the verification method used
},
}),
});
const decision = await res.json();The message the user's CKB wallet signs in the DID flow:
Veil credit decision authorization did:<did:veil:0x...> challenge:<challengeHex> verificationMethod:<did:veil:0x...#ckb-owner-1> registryVersion:1 purpose:credit-decision
Legacy Field-Based Flow
If the user has not yet registered their DID on Midnight (older accounts), you can fall back to passing the individual identity fields. This flow requires userPk, veilIdHash, sporeId, and userCkbAddress explicitly, and uses an older message format.
Veil credit decision authorization challenge:<challengeHex> userPk:<userPkHex> veilIdHash:<veilIdHash> sporeId:<sporeId>
{
"success": true,
"approved": true,
"scoreBand": "gold",
"maxLtvBps": 6500,
"riskPremiumBps": 150,
"hasCreditScore": true,
"reason": "Approved for gold tier",
"veilIdHash": "0x...",
"validAt": "2025-06-07T12:00:00.000Z"
}The decision response contains:
approved—trueif the user has a valid identity pass and meets the current decision policyscoreBand— the trust tier:unranked·bronze·silver·gold·platinummaxLtvBpsandriskPremiumBps— lending policy outputs derived from the band (LTV in basis points, e.g. 6500 = 65%)validAt— ISO 8601 timestamp of when the decision was issued
401 error.Step 5: Poll Job Status
Scoring events are queued as jobs. ZK proof generation on Midnight takes seconds to minutes depending on network load. Poll the job endpoint until the status is succeeded or failed.
| Status | Meaning | Action |
|---|---|---|
pending | Queued or proof in progress | Continue polling every 3–5 s |
succeeded | Event committed on-chain | Done — move on |
failed | Error during processing | Check error field; retry with backoff |
TypeScript Example
A complete integration showing event submission, job polling, and credit decision using the DID flow:
const VEIL_API = process.env.VEIL_API_URL ?? 'https://api.13-61-145-21.sslip.io/api/v1';
// Submit any scoring event
async function submitRepayment(params: {
userPk: string;
issuerPk: string;
paidOnTimeFlag: 0 | 1;
amountWeight: number;
eventEpoch: number;
}): Promise<string> {
const res = await fetch(`${VEIL_API}/scoring-events/repayments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!res.ok) throw new Error(`Submit failed: ${res.status}`);
const { job } = await res.json();
return job.id;
}
// Poll job until settled
async function waitForJob(jobId: string, timeoutMs = 60_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const { job } = await fetch(`${VEIL_API}/jobs/${jobId}`).then((r) => r.json());
if (job.status === 'succeeded') return;
if (job.status === 'failed') throw new Error(job.error ?? 'Job failed');
await new Promise((r) => setTimeout(r, 3000));
}
throw new Error('Timed out waiting for job');
}
// Verify user trust using their Veil DID (recommended)
async function verifyWithDid(
veilDid: string,
ckbSigner: CkbSigner, // your CCC-compatible signer
): Promise<{ approved: boolean; scoreBand: string }> {
// 1. Obtain fresh challenge
const { challenge } = await fetch(`${VEIL_API}/challenges`, { method: 'POST' }).then((r) => r.json());
// 2. Build the DID authorization message
const verificationMethod = `${veilDid}#ckb-owner-1`;
const message = [
'Veil credit decision authorization',
`did:${veilDid}`,
`challenge:${challenge}`,
`verificationMethod:${verificationMethod}`,
'registryVersion:1',
'purpose:credit-decision',
].join('\n');
// 3. Have the user sign with their CKB wallet
const authorization = await ckbSigner.signMessage(message);
// 4. Request the credit decision
const res = await fetch(`${VEIL_API}/credit-decisions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ did: veilDid, challenge, authorization: { ...authorization, verificationMethod } }),
});
if (!res.ok) throw new Error(`Decision failed: ${res.status}`);
return res.json();
}Policy Examples
Use the scoreBand from the credit decision to gate privileged actions in your protocol:
| Policy | Recommended tier |
|---|---|
| Undercollateralized borrowing | Platinum only |
| Preferential interest rate | Gold or Platinum |
| Reduced liquidation buffer | Gold or Platinum |
| Basic credit access | Bronze or above |
| Protocol access gating | Silver or above |
Error Handling
| Status | Meaning | Action |
|---|---|---|
400 | Missing or invalid request fields | Check request schema and required fields |
401 | Invalid challenge, expired, or bad signature | Re-obtain a fresh challenge and re-sign |
404 | Job, user, or DID not found | Verify the DID or userPk; user may not be registered |
409 | DID found but not yet registered on Midnight | User should complete the dashboard flow; DID registry is pending |
500 | Backend or Midnight node error | Retry with exponential backoff |