Skip to main content
This endpoint returns the user’s actual KYC data. Only call this after verifying grants via /kyc/validate.

Response Signing

All KYC data responses must be signed by your private key. Recipients verify the signature using your public key from /metadata.json. This proves:
  • The data came from your KYC provider
  • The data hasn’t been tampered with
  • You attested to the verification at the specified time

Data Structure

The data object contains fields based on the requested grants:

Identity Fields

{
  "name": {
    "first_name": "John",
    "last_name": "Doe",
    "full_name": "John Michael Doe"
  },
  "email": "john.doe@example.com",
  "phone": "+1-555-123-4567",
  "date_of_birth": "1990-05-15",
  "nationality": "US",
  "address": {
    "street": "123 Main Street",
    "city": "San Francisco",
    "state": "CA",
    "postal_code": "94102",
    "country": "US"
  }
}

Document Fields

{
  "id_card": {
    "type": "national_id",
    "number": "XXX-XX-1234",
    "country": "US",
    "issued_at": "2020-01-15",
    "expires_at": "2030-01-15",
    "image_url": "https://kyc.provider.example/secure/docs/id_abc123.jpg"
  },
  "passport": {
    "type": "passport",
    "number": "XXXXXXXXX",
    "country": "US",
    "issued_at": "2019-06-01",
    "expires_at": "2029-06-01",
    "image_url": "https://kyc.provider.example/secure/docs/passport_abc123.jpg"
  }
}

Verification Fields

{
  "liveness": {
    "verified": true,
    "selfie_url": "https://kyc.provider.example/secure/docs/selfie_abc123.jpg",
    "verified_at": 1706500000
  },
  "aml": {
    "status": "clear",
    "checked_at": 1706500000
  }
}

Implementation

app.post('/kyc/data', verifyAuth, async (req, res) => {
  const requestingOcid = req.headers['x-oc-id'];
  const { user_ocid, grants } = req.body;

  // Verify user exists
  const userKyc = await db.getUserKyc(user_ocid);
  if (!userKyc) {
    return res.status(404).json({
      error: { code: 'USER_NOT_FOUND', message: 'User not found' }
    });
  }

  // Verify grants for this OCID
  const userGrants = await db.getKycGrants(user_ocid, requestingOcid);

  // Check all requested grants are authorized
  for (const grant of grants) {
    if (!userGrants.granted?.includes(grant)) {
      return res.status(403).json({
        error: {
          code: 'GRANT_NOT_AUTHORIZED',
          message: `Grant '${grant}' not authorized`,
          details: { grant }
        }
      });
    }
  }

  // Build response data
  const data = {};
  for (const grant of grants) {
    data[grant] = userKyc.items[grant].value;

    // Generate secure URLs for documents
    if (data[grant]?.image_url) {
      data[grant].image_url = await generateSecureUrl(
        data[grant].image_url,
        { expires_in: 3600 } // 1 hour
      );
    }
  }

  const issuedAt = Math.floor(Date.now() / 1000);
  const expiresAt = issuedAt + 3600; // Data valid for 1 hour

  // Sign the data
  const dataToSign = { user_ocid, data, issued_at: issuedAt, expires_at: expiresAt };
  const canonical = JSON.stringify(dataToSign, Object.keys(dataToSign).sort());
  const signature = secp256k1_sign(PRIVATE_KEY, SHA256(canonical));

  res.json({
    user_ocid,
    data,
    issued_at: issuedAt,
    expires_at: expiresAt,
    signature
  });
});

Verifying the Signature

Recipients should verify the KYC data signature:
async function verifyKycData(kycProviderOcid, kycDataResponse) {
  // Fetch KYC provider's public key
  const metadata = await fetchOcidMetadata(kycProviderOcid);
  const publicKey = metadata.config.publicKey;

  // Reconstruct signed data
  const { signature, ...dataToVerify } = kycDataResponse;
  const canonical = JSON.stringify(dataToVerify, Object.keys(dataToVerify).sort());

  // Verify signature
  const isValid = secp256k1_verify(publicKey, SHA256(canonical), signature);

  if (!isValid) {
    throw new Error('Invalid KYC data signature');
  }

  // Check expiration
  if (kycDataResponse.expires_at < Math.floor(Date.now() / 1000)) {
    throw new Error('KYC data has expired');
  }

  return kycDataResponse.data;
}

Security Considerations

  • Document image URLs are time-limited and should expire within 1 hour
  • Sensitive fields like full ID numbers may be partially redacted
  • Cache KYC data responses only until expires_at
  • Always verify signatures before trusting the data