Tokenization
Overview
The tokenization feature enables the creation of Digital Product Passports (DPPs), represented as NFTs on compatible blockchains (Starknet, EVM, Stellar). These passports ensure product traceability, authenticity, and post-sale engagement.
This guide walks you through the entire process: creating product sheets, minting passports, and enabling users to claim them. The tokenization engine is built around three core concepts:
Product Sheets
A product sheet defines the template for your digital passports, including metadata schema, network configuration, and certification paths.
Passports
A passport is a unique NFT representing a single product. It holds product-specific data conforming to the product sheet schema and can be claimed by end users.
Claiming
Users claim passports via email authorization or magic tokens (QR codes/links). Claiming mints the NFT to the user's wallet on the blockchain.
In summary: Product Sheets define the template and blockchain configuration for your digital passports. Passports are unique NFT instances containing product-specific data. Claiming allows end users to authorize and mint these NFTs to their wallets via email verification or magic tokens.
Getting Started
Prerequisites
Before you begin, ensure you have:
- A Keyban organization account
- An application created in the admin panel
- Your API key (found in Settings > API Keys)
Authentication
All API requests require authentication via the Authorization header:
curl https://api.keyban.io/v1/dpp/products \
-H "Authorization: Bearer YOUR_API_KEY"
Your First Request
Let's retrieve the list of product sheets in your organization:
curl https://api.keyban.io/v1/dpp/products \
-H "Authorization: Bearer YOUR_API_KEY"
Expected Response:
{
"data": [],
"total": 0
}
Product Sheets
A product sheet defines the template for your digital passports. It specifies:
- The blockchain network where passports will be minted
- The data schema for both product-level and passport-level fields
- Which fields are certified on-chain
Product Sheet Status Lifecycle
Product sheets have four possible statuses:
| Status | Description |
|---|---|
DRAFT | Initial state. Not visible to end users. |
ACTIVE | Published and available for minting. |
UNLISTED | Hidden from listings but accessible via direct link. |
ARCHIVED | Deprecated. No longer claimable. |
Create a Product Sheet
Creates a new product sheet template.
Endpoint: POST /v1/dpp/products
API Reference: View full parameters and response schema
Example:
curl -X POST https://api.keyban.io/v1/dpp/products \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"application": "your-app-uuid",
"network": "StarknetSepolia",
"name": "Premium T-Shirt Collection",
"productFields": [
{ "name": "brand", "type": "string", "required": true },
{ "name": "material", "type": "string" }
],
"passportsFields": [
{ "name": "serialNumber", "type": "string", "required": true },
{ "name": "size", "type": "enum", "variants": ["S", "M", "L", "XL"] }
],
"certifiedPaths": ["brand"],
"fields": {
"brand": "Keyban Fashion",
"material": "100% Organic Cotton"
}
}'
The response includes the created product sheet with its id for subsequent operations.
List and Get Product Sheets
List all product sheets:
curl "https://api.keyban.io/v1/dpp/products?pageSize=20" \
-H "Authorization: Bearer YOUR_API_KEY"
Get a single product sheet (public endpoint, no authentication required):
curl https://api.keyban.io/v1/dpp/products/prod-123-uuid
Using the SDK:
import { useKeybanProduct } from "@keyban/sdk-react";
const [product, error] = useKeybanProduct("prod-123-uuid");
See
useKeybanProductfor complete hook documentation.
Update and Delete
Update a product sheet:
curl -X PATCH https://api.keyban.io/v1/dpp/products/prod-123-uuid \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "ACTIVE",
"fields": { "brand": "Keyban Fashion 2.0", "material": "100% Organic Cotton" }
}'
Delete a product sheet (only possible if no passports have been created):
curl -X DELETE https://api.keyban.io/v1/dpp/products/prod-123-uuid \
-H "Authorization: Bearer YOUR_API_KEY"
Statistics
Global statistics across all product sheets:
curl https://api.keyban.io/v1/dpp/products/stats \
-H "Authorization: Bearer YOUR_API_KEY"
Product-specific statistics:
curl https://api.keyban.io/v1/dpp/products/prod-123-uuid/stats \
-H "Authorization: Bearer YOUR_API_KEY"
API Reference: View all statistics endpoints
Passports
A passport is a unique NFT instance representing a single product. Each passport:
- Belongs to a product sheet
- Contains data validated against the product's
passportsFieldsschema - Has a unique
tokenId(SHA256 hash of the passport ID) - Can optionally be restricted to a specific email for claiming
Create a Passport
Creates a single passport for a product sheet.
Endpoint: POST /v1/dpp/passports
API Reference: View full parameters and response schema
Example:
curl -X POST https://api.keyban.io/v1/dpp/passports \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"product": "prod-123-uuid",
"fields": {
"serialNumber": "SN-001-2024",
"size": "M"
},
"allowedClaimEmail": "customer@example.com"
}'
List and Get Passports
List passports for a product:
curl "https://api.keyban.io/v1/dpp/passports?filters[product.id][eq]=prod-123-uuid" \
-H "Authorization: Bearer YOUR_API_KEY"
Get a single passport (public endpoint):
curl https://api.keyban.io/v1/dpp/passports/pass-456-uuid
Using the SDK:
import { useKeybanPassport } from "@keyban/sdk-react";
const [passport, error] = useKeybanPassport("pass-456-uuid");
See
useKeybanPassportfor complete hook documentation.
Update and Delete
Update a passport:
curl -X PATCH https://api.keyban.io/v1/dpp/passports/pass-456-uuid \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fields": { "serialNumber": "SN-001-2024-REV", "size": "L" },
"allowedClaimEmail": "new-customer@example.com"
}'
Delete a passport (only possible if not minted on-chain):
curl -X DELETE https://api.keyban.io/v1/dpp/passports/pass-456-uuid \
-H "Authorization: Bearer YOUR_API_KEY"
Magic Token
Generate a single-use magic token for claiming a passport without email verification:
curl https://api.keyban.io/v1/dpp/passports/pass-456-uuid/magic-token \
-H "Authorization: Bearer YOUR_API_KEY"
This token can be embedded in QR codes or claim links for distribution.
Batch Minting
For large-scale passport creation, use the batch minting endpoint. This creates multiple passports and mints them on the blockchain in a single operation.
Start a Mint Job
Endpoint: POST /v1/dpp/products/:id/mint
API Reference: View full parameters
Example:
curl -X POST https://api.keyban.io/v1/dpp/products/prod-123-uuid/mint \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{ "serialNumber": "SNK-001", "size": "42" },
{ "serialNumber": "SNK-002", "size": "43" },
{ "serialNumber": "SNK-003", "size": "41" }
]'
Response:
{
"jobId": "job-abc-123"
}
Track Mint Job Status
Poll for status:
curl https://api.keyban.io/v1/dpp/products/prod-123-uuid/mint/job-abc-123/status \
-H "Authorization: Bearer YOUR_API_KEY"
Real-time updates with SSE:
curl https://api.keyban.io/v1/dpp/products/prod-123-uuid/mint/job-abc-123/status/sse \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: text/event-stream"
Claiming Passports
Users can claim passports through two mechanisms:
| Method | Use Case | Requires |
|---|---|---|
| Email-based | Known customers | allowedClaimEmail set on passport |
| Magic Token | Unknown customers, POS, QR codes | Magic token generated for passport |
Email-Based Claiming
The user must be authenticated via the SDK and their email must match the passport's allowedClaimEmail.
import { useKeybanClient } from "@keyban/sdk-react";
function ClaimWithEmail({ productId, passportId }) {
const client = useKeybanClient();
const handleClaim = async () => {
try {
// User email must match passport.allowedClaimEmail
const result = await client.api.dpp.claim(productId, passportId);
console.log("Claimed! Transaction:", result.transactionHash);
} catch (error) {
console.error("Claim failed:", error.message);
}
};
return <button onClick={handleClaim}>Claim Your Passport</button>;
}
Magic Token Claiming
Magic tokens allow claiming without email verification. Useful for:
- QR codes on physical products
- Claim links in marketing emails
- Point-of-sale scenarios
Flow:
- Organization generates magic token:
GET /v1/dpp/passports/:id/magic-token - Token is distributed (QR code, link, etc.)
- User claims with token:
POST /v1/dpp/claim/magicToken
import { useKeybanClient } from "@keyban/sdk-react";
function ClaimWithMagicToken({ productId, magicToken }) {
const client = useKeybanClient();
const handleClaim = async () => {
try {
const result = await client.api.dpp.magicClaim(productId, magicToken);
console.log("Claimed! Transaction:", result.transactionHash);
} catch (error) {
console.error("Claim failed:", error.message);
}
};
return <button onClick={handleClaim}>Claim via Magic Link</button>;
}
Track Claim Job Status
Claims are processed asynchronously. Track progress with:
- Poll:
GET /v1/dpp/claim/:jobId/status - SSE:
GET /v1/dpp/claim/:jobId/status/sse
SDK Integration
The Keyban SDK provides React hooks for working with product sheets and passports.
Fetching Data
import { useKeybanProduct, useKeybanPassport } from "@keyban/sdk-react";
function ProductDetails({ productId }) {
const [product, error] = useKeybanProduct(productId);
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>{product.name}</h2>
<p>Network: {product.network}</p>
<p>Status: {product.status}</p>
</div>
);
}
function PassportDetails({ passportId }) {
const [passport, error] = useKeybanPassport(passportId);
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h3>Passport: {passport.id}</h3>
<p>Token ID: {passport.tokenId}</p>
<pre>{JSON.stringify(passport.fields, null, 2)}</pre>
</div>
);
}
Complete Claim Flow
import React, { Suspense, useState } from "react";
import { KeybanProvider, useKeybanClient } from "@keyban/sdk-react";
function ClaimPage({ productId, passportId, magicToken }) {
const client = useKeybanClient();
const [status, setStatus] = useState("idle");
const handleEmailClaim = async () => {
setStatus("claiming");
try {
const result = await client.api.dpp.claim(productId, passportId);
setStatus(`Success! TX: ${result.transactionHash}`);
} catch (error) {
setStatus(`Error: ${error.message}`);
}
};
const handleMagicClaim = async () => {
setStatus("claiming");
try {
const result = await client.api.dpp.magicClaim(productId, magicToken);
setStatus(`Success! TX: ${result.transactionHash}`);
} catch (error) {
setStatus(`Error: ${error.message}`);
}
};
return (
<div>
<p>Status: {status}</p>
{magicToken ? (
<button onClick={handleMagicClaim}>Claim with Magic Link</button>
) : (
<button onClick={handleEmailClaim}>Claim with Email</button>
)}
</div>
);
}
export function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<KeybanProvider appId="your-app-id" network="StarknetSepolia">
<ClaimPage productId="..." passportId="..." />
</KeybanProvider>
</Suspense>
);
}
Troubleshooting
Common Error Codes
| Code | Meaning | Common Causes |
|---|---|---|
| 400 | Bad Request | Missing required fields, invalid JSON format |
| 401 | Unauthorized | Missing or invalid API key |
| 403 | Forbidden | Insufficient permissions for the resource |
| 404 | Not Found | Resource doesn't exist or belongs to another organization |
| 409 | Conflict | Duplicate resource (e.g., duplicate shopifyId) |
| 422 | Validation Error | Field values don't match the expected schema |
Common Issues
"Passport cannot be deleted"
- The passport has already been minted on the blockchain
- Solution: Passports cannot be deleted after minting. Consider archiving the product instead.
"Product cannot be deleted"
- The product sheet has associated passports
- Solution: Delete all passports first, or change the product status to
ARCHIVED.
"Email not authorized to claim"
- The authenticated user's email doesn't match
allowedClaimEmail - Solution: Update the passport's
allowedClaimEmailor use magic token claiming.
"Magic token invalid"
- The token has expired, been used, or is malformed
- Solution: Generate a new magic token for the passport.
API Reference
For complete endpoint documentation including all parameters, request/response schemas, and error codes:
- Digital Product Passport API — Product sheets, passports, minting, claiming
Quick Reference
| Resource | Create | Read | Update | Delete |
|---|---|---|---|---|
| Product Sheets | POST /v1/dpp/products | GET /v1/dpp/products/:id | PATCH | DELETE |
| Passports | POST /v1/dpp/passports | GET /v1/dpp/passports/:id | PATCH | DELETE |
| Claims | POST /v1/dpp/claim | GET .../status | — | — |
SDK Hooks
| Hook | Description |
|---|---|
useKeybanProduct | Fetch product sheet data |
useKeybanPassport | Fetch passport data |
useKeybanClient | Access API client for claiming |
For complete SDK documentation, see the React SDK Reference.