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.

POST /v1/templates
{
  "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.

POST /v1/tokens/{template_id}/actions/mint
{
  "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.

POST /v1/objects/{object_id}/actions/{action_name}
{
  "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).

POST /v1/tokens/{template_id}/batch/mint
{
  "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.

GET /v1/objects/{object_id}
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)

GET /v1/objects?template_id={template_id}&filter.mutable.custody_status=at_pharmacy&limit=100&cursor=xyz
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.

POST /v1/objects/{object_id}/actions/transfer
{
  "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.

POST /v1/webhooks
{
  "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.

GET /v1/objects/{object_id}/history?limit=100&order=desc
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.

POST /v1/objects/{object_id}/compliance/validate
{
  "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.

GET /v1/objects/stats?template_id={template_id}
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.