Skip to main content

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:

StatusDescription
DRAFTInitial state. Not visible to end users.
ACTIVEPublished and available for minting.
UNLISTEDHidden from listings but accessible via direct link.
ARCHIVEDDeprecated. 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 useKeybanProduct for 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 passportsFields schema
  • 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 useKeybanPassport for 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:

MethodUse CaseRequires
Email-basedKnown customersallowedClaimEmail set on passport
Magic TokenUnknown customers, POS, QR codesMagic 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:

  1. Organization generates magic token: GET /v1/dpp/passports/:id/magic-token
  2. Token is distributed (QR code, link, etc.)
  3. 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

CodeMeaningCommon Causes
400Bad RequestMissing required fields, invalid JSON format
401UnauthorizedMissing or invalid API key
403ForbiddenInsufficient permissions for the resource
404Not FoundResource doesn't exist or belongs to another organization
409ConflictDuplicate resource (e.g., duplicate shopifyId)
422Validation ErrorField 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 allowedClaimEmail or 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:

Quick Reference

ResourceCreateReadUpdateDelete
Product SheetsPOST /v1/dpp/productsGET /v1/dpp/products/:idPATCHDELETE
PassportsPOST /v1/dpp/passportsGET /v1/dpp/passports/:idPATCHDELETE
ClaimsPOST /v1/dpp/claimGET .../status

SDK Hooks

HookDescription
useKeybanProductFetch product sheet data
useKeybanPassportFetch passport data
useKeybanClientAccess API client for claiming

For complete SDK documentation, see the React SDK Reference.