DUAL API Recipes
Reusable patterns that power all 30 tokenization concepts
Reusable patterns that power all 30 tokenization concepts
These 10 recipes cover ~95% of implementations across concepts. Each recipe shows exact HTTP endpoints, payloads, and responses.
Prerequisites: API credentials with template creation, object management, and webhook permissions.
Recipe 1: Create a Template
Used by: Every concept. Defines token schema, state machine, and valid actions.
{
"name": "DrugBatch",
"description": "Serialized drug custody token for DSCSA compliance",
"object": {
"metadata": {
"name": "Drug Supply Chain Token",
"category": "pharmaceuticals"
},
"immutable": {
"drug_family": { "type": "string" },
"batch_number": { "type": "string" },
"manufacturer_hash": { "type": "string" }
},
"mutable": {
"custody_status": {
"type": "enum",
"values": ["manufactured", "in_transit", "at_pharmacy", "dispensed"]
},
"wholesaler_signature": { "type": "string" }
}
},
"state_transitions": {
"manufactured": ["in_transit"],
"in_transit": ["at_pharmacy"],
"at_pharmacy": ["dispensed"],
"dispensed": []
}
}
Response:
{
"id": "tmpl_drugbatch_v1",
"name": "DrugBatch",
"created_at": "2026-04-09T14:35:00Z",
"status": "active"
}
Key principle: Template mutable section IS your mutable state. Every field that changes during lifecycle must be here.
Recipe 2: Mint a Token (Create Object)
Used by: Every concept when an asset/claim/credential enters the system.
{
"immutable": {
"drug_family": "Ozempic 1mg",
"batch_number": "OZ-2026-04-001",
"manufacturer_hash": "sha256:abc123..."
},
"mutable": {
"custody_status": "manufactured",
"wholesaler_signature": ""
},
"owner_wallet": "0xmanufacturer123..."
}
Response:
{
"object_id": "obj_drugbatch_001",
"template_id": "tmpl_drugbatch_v1",
"batch_id": "batch_mint_001",
"status": "processing",
"immutable_hash": "0xabc123...",
"state_root": "0xdef456..."
}
Pattern: Mint with initial state. Token is now live and immutably registered.
Recipe 3: Execute a State Transition
Used by: Every concept. Moves tokens through their lifecycle.
{
"mutable": {
"custody_status": "in_transit",
"wholesaler_signature": "sig:wholesaler_..."
},
"metadata": {
"handoff_timestamp": "2026-04-08T14:30:00Z",
"location": { "lat": 37.7749, "lng": -122.4194 }
}
}
Response:
{
"object_id": "obj_drugbatch_001",
"status": "in_transit",
"batch_id": "batch_transit_001",
"timestamp": "2026-04-08T14:31:15Z",
"state_root": "0xghi789..."
}
Pattern: Actions update mutable fields and create immutable audit entries. Chain multiple actions for cascading workflows.
Recipe 4: Batch Mint Tokens (Parallel Creation)
Used by: Concepts requiring bulk creation (authentication, credentialing, asset onboarding).
{
"batch_size": 100,
"tokens": [
{
"immutable": {
"drug_family": "Ozempic 1mg",
"batch_number": "OZ-2026-04-001",
"manufacturer_hash": "sha256:abc123..."
},
"mutable": {
"custody_status": "manufactured"
},
"owner_wallet": "0xmanufacturer123..."
},
{
"immutable": {
"drug_family": "Ozempic 2mg",
"batch_number": "OZ-2026-04-002",
"manufacturer_hash": "sha256:def456..."
},
"mutable": {
"custody_status": "manufactured"
},
"owner_wallet": "0xmanufacturer123..."
}
],
"idempotency_key": "batch_2026_04_001"
}
Response:
{
"batch_id": "batch_mint_001",
"total_count": 100,
"status": "processing",
"created_count": 100,
"object_ids": ["obj_001", "obj_002", ...],
"estimated_completion": "2026-04-09T14:45:00Z"
}
Idempotency: Use idempotency_key to safely retry failed batches without duplicating tokens.
Recipe 5: Query Token State (Read Current Status)
Used by: Dashboards, verification, audit views, real-time monitoring.
Returns:
{
"id": "obj_drugbatch_001",
"template_id": "tmpl_drugbatch_v1",
"created_at": "2026-04-09T14:32:15Z",
"immutable": {
"drug_family": "Ozempic 1mg",
"batch_number": "OZ-2026-04-001"
},
"mutable": {
"custody_status": "at_pharmacy",
"wholesaler_signature": "sig:..."
},
"owner": "0xpharmacy123...",
"state_root": "0xabc123...",
"transitions_count": 3
}
Bulk Query (List by Filter)
Returns:
{
"count": 42,
"next_cursor": "cursor_abc123...",
"objects": [
{ "id": "obj_001", "mutable": { "custody_status": "at_pharmacy" } },
{ "id": "obj_002", ... }
]
}
Pagination: Use limit=100 and cursor for large datasets. Never fetch all objects without pagination.
Recipe 6: Transfer Ownership (Chain of Custody)
Used by: LuxeVerify, PharmChain, WarrantyChain, FleetIQ, HarvestChain, AquaAlloc.
{
"to_wallet": "0xnew_owner_123...",
"transfer_reason": "custody_handoff",
"mutable": {
"current_owner": "0xnew_owner_123...",
"previous_owners": ["0xold_owner_1...", "0xold_owner_2..."]
},
"signatures": {
"sender": "sig:sender_confirms...",
"recipient": "sig:recipient_confirms..."
}
}
Response:
{
"object_id": "obj_drugbatch_001",
"previous_owner": "0xold_owner_1...",
"new_owner": "0xnew_owner_123...",
"batch_id": "batch_transfer_001",
"timestamp": "2026-04-09T16:00:15Z",
"state_root": "0xdef456..."
}
Pattern: Transfers create immutable provenance records. Combined with state transitions, gives full lifecycle tracking and verifiable chain-of-custody.
Recipe 7: Set Up Webhooks (Event-Driven Integration)
Used by: Payment release triggers, compliance alerts, IoT data ingestion, real-time dashboards.
{
"name": "pharma_supply_events",
"url": "https://api.yourcompany.io/webhooks/dual",
"events": [
"action.executed:mint",
"action.executed:transfer",
"action.executed:dispense"
],
"template_id": "tmpl_drugbatch_v1",
"signing_secret": "whsec_live_123...",
"retry_policy": {
"max_attempts": 3,
"backoff_multiplier": 2,
"max_backoff_seconds": 300
}
}
Response:
{
"id": "hook_pharma_supply",
"status": "active",
"created_at": "2026-04-09T18:00:00Z",
"test_event_sent": true
}
Webhook Payload Format
{
"event_id": "evt_12345678",
"event_type": "action.executed",
"action_name": "transfer",
"object_id": "obj_drugbatch_001",
"template_id": "tmpl_drugbatch_v1",
"batch_id": "batch_transfer_001",
"timestamp": "2026-04-09T16:00:15Z",
"actor": "0xpharmacy123...",
"immutable": { ... },
"mutable": { ... },
"mutable_changes": {
"custody_status": "at_pharmacy",
"current_owner": "0xpharmacy123..."
},
"state_root": "0xdef456..."
}
Verification (Node.js):
const crypto = require('crypto');
const signature = req.headers['x-dual-signature'];
const payload = JSON.stringify(req.body);
const hash = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (hash !== signature) return res.status(401).send('Unauthorized');
Always verify signatures. Never process a webhook without validating x-dual-signature HMAC.
Recipe 8: Get Full Audit Trail (Token History)
Used by: Compliance audits, proof of authenticity, litigation discovery.
Returns:
{
"object_id": "obj_drugbatch_001",
"total_events": 5,
"events": [
{
"timestamp": "2026-04-09T16:00:15Z",
"action": "transfer",
"batch_id": "batch_transfer_001",
"actor": "0xwholesaler123...",
"state_before": "in_transit",
"state_after": "at_pharmacy",
"mutable_changes": {
"custody_status": "at_pharmacy",
"current_owner": "0xpharmacy123..."
},
"immutable_hash": "0xabc123...",
"signatures": {
"transaction": "0xtx_hash_123..."
}
},
{
"timestamp": "2026-04-08T14:31:15Z",
"action": "transfer",
"batch_id": "batch_transit_001",
"actor": "0xmanufacturer123...",
"state_before": "manufactured",
"state_after": "in_transit",
"mutable_changes": { "custody_status": "in_transit" }
},
...
]
}
Immutable record: Every state change is cryptographically signed and irreversible. Perfect for compliance audits and provenance verification.
Recipe 9: Compliance Gate (Conditional Transitions)
Used by: Any concept requiring regulatory checks before state transitions.
{
"action": "transfer",
"to_wallet": "0xbuyer_123...",
"checks": {
"ofac_sanctions": true,
"export_control": true,
"tax_jurisdiction": "US",
"anti_counterfeiting": true
}
}
Response:
{
"object_id": "obj_item_001",
"action": "transfer",
"passed": true,
"checks": {
"ofac_sanctions": { "passed": true, "checked_at": "2026-04-09T15:55:00Z" },
"export_control": { "passed": true, "jurisdictions_allowed": ["US", "CA"] },
"anti_counterfeiting": { "passed": true, "confidence": 0.998 }
},
"safe_to_execute": true,
"timestamp": "2026-04-09T15:55:15Z"
}
Example: Blocking Export-Restricted Transfers
// Pseudo-code: Check before executing transfer
const compliance = await client.validateCompliance({
action: 'transfer',
to_wallet: buyer,
checks: { export_control: true }
});
if (!compliance.safe_to_execute) {
return error(`Transfer blocked: ${compliance.checks.export_control.reason}`);
}
// Only execute if compliance clears
await client.executeAction('transfer', { to_wallet: buyer });
Pattern: Always validate compliance before irreversible transitions. Use webhooks to automate compliance checks.
Recipe 10: Query Analytics (Dashboard Metrics)
Used by: Every concept for reporting, KPI dashboards, health monitoring.
Returns:
{
"template_id": "tmpl_drugbatch_v1",
"total_objects": 1250,
"by_status": {
"manufactured": 45,
"in_transit": 230,
"at_pharmacy": 950,
"dispensed": 25
},
"by_owner": [
{ "owner": "0xmanufacturer123...", "count": 50 },
{ "owner": "0xwholesaler123...", "count": 200 },
{ "owner": "0xpharmacy123...", "count": 1000 }
],
"state_change_timelines": {
"manufactured_to_in_transit": { "avg_hours": 2.3 },
"in_transit_to_at_pharmacy": { "avg_hours": 48.1 },
"at_pharmacy_to_dispensed": { "avg_hours": 12.5 }
}
}
GET /v1/actions/stats?template_id={template_id}&action=transfer&interval=1h
Returns:
{
"timeline": [
{
"period": "2026-04-09T15:00:00Z",
"count": 42,
"avg_duration_ms": 125
},
{
"period": "2026-04-09T16:00:00Z",
"count": 58,
"avg_duration_ms": 118
}
],
"total_count": 1250,
"avg_duration_ms": 121
}
Dashboard pattern: Combine stats queries with filtered object lists to build concept-specific KPI dashboards.
Common Implementation Pattern
Every DUAL concept follows the same 5-step workflow:
1. Define template (Recipe 1) — Encode state machine as object.mutable + state transitions
2. Mint tokens (Recipe 2, 4) — Create instances when assets/claims enter system
3. Execute transitions (Recipe 3) — Move tokens through lifecycle via actions
4. Wire webhooks (Recipe 7) — Connect DUAL events to external systems (payments, notifications, etc.)
5. Query state (Recipe 5, 8, 10) — Build dashboards, audits, health views
The differentiation between concepts is in the template schema and action definitions, not the integration pattern. This is why DUAL powers 30 different verticals with the same core API.
Real-World Example: PharmChain Implementation
Step 1: Define Template
{
"name": "PharmChain",
"mutable": {
"custody_status": { "type": "enum", "values": ["manufactured", "in_transit", "at_pharmacy", "dispensed"] },
"wholesaler_signature": { "type": "string" }
}
}
Step 2: Batch Mint
POST /v1/tokens/tmpl_drugbatch_v1/batch/mint
{
"batch_size": 1000,
"tokens": [
{ "immutable": { "drug_family": "Ozempic 1mg", "batch_number": "OZ-2026-04-001" }, ... },
...
]
}
Step 3: Execute State Transition
POST /v1/objects/obj_drugbatch_001/actions/transfer
{
"to_wallet": "0xwholesaler123...",
"mutable": { "custody_status": "in_transit" }
}
Step 4: Wire Webhook
POST /v1/webhooks
{
"url": "https://pharmacy.io/webhooks/arrivals",
"events": ["action.executed:transfer"],
"filters": { "template_id": "tmpl_drugbatch_v1" }
}
Step 5: Dashboard Metrics
GET /v1/objects/stats?template_id=tmpl_drugbatch_v1
Result: Total objects: 1250, In_transit: 230, At_pharmacy: 950
Avg custody transfer time: 2.3 hours
Design Principles
Principle 1: Immutable by Default
Immutable fields never change. Use them for proofs, hashes, authentication data. This ensures cryptographic commitment and regulatory compliance.
Principle 2: Mutable State via Actions
Never directly modify mutable fields. Only change them via actions. Actions create audit trail entries and trigger webhooks.
Principle 3: Webhooks are Your Integration Layer
Never poll DUAL for state. Set up webhooks. When something happens, your systems get notified instantly.
Principle 4: Pagination for Scale
Always use pagination (limit, cursor) when querying objects. For 100K+ items, never fetch without limits.
Principle 5: Idempotency Keys for Safety
Use idempotency_key on batch operations. Allows safe retry of failed batches without duplicating tokens.
Error Handling & Retry Strategy
| Status Code | Meaning | Action |
|---|---|---|
| 200 / 201 | Success | Process response normally |
| 400 | Bad request (schema error, invalid state transition) | Fix payload; do not retry |
| 401 | Unauthorized (invalid API key) | Check credentials; do not retry |
| 409 | Conflict (state already in desired state) | Safe to ignore or retry with idempotency_key |
| 429 | Rate limited | Exponential backoff (2s, 4s, 8s) |
| 500+ | Server error | Exponential backoff; check status.dual.io |
Batch Operations: If batch mint fails midway, use the returned created_count to skip already-created tokens and resume from there.
Performance Benchmarks
| Operation | Typical Latency | Max Throughput |
|---|---|---|
| Single mint | 100-200ms | ~5K/sec |
| Batch mint (100) | 500-1000ms | ~500 batches/sec |
| State transition | 50-150ms | ~10K/sec |
| Query object | 10-30ms | ~20K/sec (cached) |
| Webhook delivery | sub-100ms | Async, at-least-once |
For 1M+ tokens: Use batch mint with 100-500 tokens per batch. Distribute across multiple API workers. Cache state roots for instant verification.