Developer documentation · API v1

Verify real cards without hosting their game.

The Open Card Passport API gives any TCG, tournament platform, deck site, marketplace, or community app a free read-only view of a physical card's mint, authentication, optional token binding, and last-confirmed token controller.

Base URL: https://halovault.ai/api/open-card-passports/v1/
Public read authentication: none
CORS: Access-Control-Allow-Origin: *

Quick start

Look up a Passport ID. Replace the example ID with one supplied by a player:

curl --fail-with-body \
  -H "Accept: application/json" \
  https://halovault.ai/api/open-card-passports/v1/passports/HV-CARD-7A2F4D91C830/

In a browser or Node application:

const id = "HV-CARD-7A2F4D91C830";
const response = await fetch(
  `https://halovault.ai/api/open-card-passports/v1/passports/${encodeURIComponent(id)}/`,
  { headers: { Accept: "application/json" } },
);

if (response.status === 404) {
  throw new Error("Unknown Card Passport");
}
if (!response.ok) {
  throw new Error(`HaloVault lookup failed: ${response.status}`);
}

const proof = await response.json();
if (!proof.passport.active || !proof.mint.valid) {
  throw new Error("Passport is not currently acceptable");
}

The API never returns legal_for_play: true. Map card to your catalog and apply your own format, ban-list, copy-limit, deck, event, and matchmaking rules.

Trust model

LayerPositive factExplicit limit
PassportA stable identity record exists for one physical card and its metadata commitment has not silently changed.Identity is not game legality.
Mint / HaloCertThe sealed record recomputes correctly on HaloVault's append-only hash chain and is not revoked.A self mint is not third-party authentication.
Store-VerifiedAn authorized verifier physically inspected, identified, and photographed the card at a recorded time.Not a grade, appraisal, legal-title guarantee, or continuous-custody proof.
Token bindingWhen verified, an external token's tokenURI resolves to the exact Passport manifest and commitment.A recorded but unverified binding is only a submitted reference.
Token controllerA read-only chain RPC last returned the stated address through ownerOf.The game must verify a fresh wallet signature; token control is not legal ownership of the physical card.
NFCAn optional tag is bound to the item/certificate.NFC is not required for an online integration and a plain UID alone is clonable.

Public endpoints

GET Service discovery

/api/open-card-passports/v1/

Returns the current schema version, endpoint URLs, cost/authentication declaration, and trust-model summary.

GET Passport ID lookup

/api/open-card-passports/v1/passports/{passport_id}/

This is the preferred lookup when the player or deck list already stores an HV-CARD-… identifier.

GET Resolve another identifier

/api/open-card-passports/v1/resolve/

Use exactly one lookup mode:

# Resolve a HaloCert mint ID
curl "https://halovault.ai/api/open-card-passports/v1/resolve/?certificate_id=HV-CERT-A1B2C3D4E5"

# Resolve an EVM token using a CAIP-2 chain tuple
curl "https://halovault.ai/api/open-card-passports/v1/resolve/?chain_namespace=eip155&chain_reference=8453&contract_address=0x0123456789abcdef0123456789abcdef01234567&token_id=42"

For an EVM asset, chain_namespace=eip155, chain_reference is the numeric chain ID, and contract_address is required.

POST Batch verification

/api/open-card-passports/v1/verify/

Accepts up to 50 mixed queries and returns results in the same order. This endpoint is read-only and intentionally CSRF-exempt so browser games can call it cross-origin.

GET Token manifest

/api/open-card-passports/v1/passports/{passport_id}/manifest/

Returns ERC-compatible token metadata containing the Passport ID, metadata hash, commitment, mint certificate ID, mint record hash, and verification API URL. Use this exact URL as an ERC-721 tokenURI when you want automated binding verification.

GET OpenAPI document

/api/open-card-passports/v1/openapi.json

Response schema

A successful lookup returns the complete current state. This condensed example shows the decision-bearing fields:

{
  "schema": "halovault.open-card-passport",
  "schema_version": "1.0",
  "found": true,
  "passport": {
    "passport_id": "HV-CARD-7A2F4D91C830",
    "status": "active",
    "active": true,
    "metadata_version": 1,
    "metadata_sha256": "…",
    "commitment": "…"
  },
  "card": {
    "game_namespace": "pokemon",
    "publisher": "The Pokemon Company",
    "game_name": "Pokemon Trading Card Game",
    "card_name": "Charizard ex",
    "set_name": "Obsidian Flames",
    "set_code": "OBF",
    "collector_number": "125/197",
    "printing": "2023",
    "variant": "Double Rare",
    "language": "en",
    "finish": "holo",
    "catalog_ids": {}
  },
  "mint": {
    "recorded": true,
    "valid": true,
    "certificate_id": "HV-CERT-A1B2C3D4E5",
    "certificate_kind": "verified_mint",
    "record_hash": "…",
    "issued_at": "2026-08-01T16:20:00+00:00",
    "verify_url": "https://halovault.ai/cert/HV-CERT-A1B2C3D4E5/",
    "integrity": {
      "valid": true,
      "hash_valid": true,
      "chain_link_valid": true,
      "revoked": false
    }
  },
  "authentication": {
    "level": "store_verified",
    "label": "Store-Verified",
    "valid": true,
    "physical_inspection": true,
    "issuer": { "name": "Example Card Shop", "tenant_id": 17 },
    "certificate_id": "HV-CERT-A1B2C3D4E5",
    "claim": "…",
    "limits": "…"
  },
  "physical_link": {
    "nfc_required": false,
    "nfc_bound": false
  },
  "onchain": {
    "bound": true,
    "verified_binding": true,
    "bindings": [
      {
        "chain": { "caip2": "eip155:8453", "network_name": "Base" },
        "asset": {
          "standard": "erc721",
          "contract_address": "0x…",
          "token_id": "42",
          "mint_tx_hash": "0x…",
          "token_uri": "https://halovault.ai/api/open-card-passports/v1/passports/HV-CARD-7A2F4D91C830/manifest/"
        },
        "binding": { "status": "verified", "verified": true },
        "token_control": {
          "address": "0x…",
          "verification": "confirmed",
          "checked_at": "2026-08-01T16:25:00+00:00",
          "source": "chain_rpc"
        }
      }
    ]
  },
  "token_control": {
    "available": true,
    "binding_verified": true,
    "address": "0x…",
    "verification": "confirmed",
    "chain": "eip155:8453",
    "checked_at": "2026-08-01T16:25:00+00:00",
    "proof_method": "The game issues its own nonce and verifies a wallet signature against this address."
  },
  "game_integration": {
    "halo_vault_hosts_gameplay": false,
    "card_legality": "not_determined_by_halovault"
  }
}
passport ├── card identity and sealed metadata ├── mint → permanent HaloCert integrity ├── authentication → strongest current evidence tier ├── physical_link → optional NFC facts ├── onchain.bindings[] → every submitted token reference ├── token_control → best current controller candidate └── game_integration → explicit non-game boundary

Recommended decision algorithm

Your policy may be different, but the following strict order prevents a stale or self-reported fact from becoming an ownership shortcut:

  1. Require HTTP 200 and found=true.
  2. Require passport.active=true and mint.valid=true.
  3. Map the card identity to your own catalog. Reject ambiguous or unknown printings.
  4. Require the authentication level appropriate for your queue—for example, store_verified for ranked physical-card play.
  5. If your experience is token-gated, require onchain.verified_binding=true.
  6. Require a suitable token_control.verification. Treat recorded, unverified, stale, error, and unsupported according to your risk policy.
  7. Issue your own single-use, expiring wallet challenge and compare its recovered signer to token_control.address.
  8. Apply your own deck legality, copy limit, format, ban list, event, and anti-cheat rules.
function evaluatePassport(proof, policy) {
  if (!proof?.found) return { ok: false, reason: "not_found" };
  if (!proof.passport.active) return { ok: false, reason: `passport_${proof.passport.status}` };
  if (!proof.mint.valid) return { ok: false, reason: "mint_invalid" };

  const allowedTiers = new Set(policy.acceptedAuthenticationLevels);
  if (!allowedTiers.has(proof.authentication.level)) {
    return { ok: false, reason: "authentication_tier_too_low" };
  }

  if (policy.requireToken) {
    if (!proof.token_control.binding_verified) return { ok: false, reason: "token_binding_unverified" };
    if (proof.token_control.verification !== "confirmed") {
      return { ok: false, reason: "token_controller_not_freshly_confirmed" };
    }
    return {
      ok: true,
      walletChallengeRequired: true,
      expectedAddress: proof.token_control.address,
      chain: proof.token_control.chain,
    };
  }
  return { ok: true, walletChallengeRequired: false };
}

Prove the player controls the wallet

The API reports public chain state; it does not authenticate the person in your session. Your application must perform the final wallet challenge.

  1. Create a cryptographically random, single-use nonce on your server.
  2. Bind it to your user/session, requested Passport ID, domain, purpose, chain, issue time, and a short expiration.
  3. Ask the wallet to sign an unambiguous message. EIP-712 typed data is preferred on EVM; a well-formed EIP-191 message is acceptable.
  4. Recover the signer server-side and compare normalized addresses with token_control.address.
  5. Consume the nonce exactly once. Never accept a signature captured for another domain, action, card, or session.
HaloVault Card Passport login
Domain: play.example.com
Purpose: enter ranked physical-card queue
Passport: HV-CARD-7A2F4D91C830
Chain: eip155:8453
Nonce: b4c9a6b69ad548e8b9914e5cf57d2c41
Issued: 2026-08-01T16:25:00Z
Expires: 2026-08-01T16:30:00Z
Do not accept a wallet address typed into a profile as proof. The signature proves control of the private key for your fresh challenge. The Card Passport response alone does not.

Batch-check a deck

curl --fail-with-body \
  -X POST \
  -H "Content-Type: application/json" \
  https://halovault.ai/api/open-card-passports/v1/verify/ \
  -d '{
    "queries": [
      {"request_id":"main-1", "passport_id":"HV-CARD-7A2F4D91C830"},
      {"request_id":"main-2", "certificate_id":"HV-CERT-A1B2C3D4E5"},
      {
        "request_id":"side-1",
        "chain_namespace":"eip155",
        "chain_reference":"8453",
        "contract_address":"0x0123456789abcdef0123456789abcdef01234567",
        "token_id":"42"
      }
    ]
  }'

Each result includes your optional request_id, its zero-based index, and either the full Passport response or found=false with an error. One bad query does not fail the other deck slots.

Bind an externally minted token

HaloVault does not require a proprietary game or custodial wallet. A compatible mint provider can bind an ERC-721—or record another chain asset—using the Passport's public metadata.

  1. Create or Store-Verify the physical card and confirm its cross-TCG identity before token minting.
  2. Fetch /passports/{passport_id}/manifest/.
  3. Use that exact HTTPS URL as the ERC-721 tokenURI. It includes the Passport's sealed commitment and HaloCert record hash.
  4. Mint to the player's non-custodial wallet through the provider or contract of your choice.
  5. From the authenticated collector/store workflow, record the CAIP-2 chain, contract, token ID, and mint transaction hash.
  6. After confirmation, run the chain sync. On a configured EVM chain, HaloVault calls ownerOf(tokenId) and tokenURI(tokenId). A matching canonical manifest changes the binding from recorded to verified.

Identity fields become locked once a non-rejected token binding exists. If a card was identified incorrectly, retire the bad binding, correct the Passport, and mint/bind a corrected token rather than silently mutating what the old token committed to.

Current automated chain check: ERC-721 ownerOf and tokenURI on EVM chains for which HaloVault has a read-only RPC URL. ERC-1155 and non-EVM assets can be recorded, but are returned as unsupported for automated single-owner checks until an appropriate adapter exists.

CORS, caching, and rate limits

These are fair-use defaults for the free API and may be tuned for platform protection. A game should cache stable identity fields but refresh status and token-control facts near the action that depends on them.

Errors and status codes

HTTPErrorMeaning
400one_lookup_mode_requiredThe resolve query mixed identifiers or supplied none.
400incomplete_chain_lookupA chain tuple is missing required fields.
400queries_requiredThe batch body did not contain a non-empty array.
400batch_too_largeMore than 50 queries were submitted.
404passport_not_foundNo public Passport matches the identifier.
405method_not_allowedThe endpoint does not support that HTTP method.
413request_too_largeThe batch body exceeded 64 KiB.
429rate_limitedWait for the provided Retry-After duration.

Business state such as revoked, disputed, stale, or recorded is returned in a normal 200 response when the Passport exists. That lets your policy distinguish a known-but-unacceptable card from an unknown ID.

Security and privacy notes

Versioning and compatibility

The URL and top-level response carry major version v1/schema_version: "1.0". HaloVault may add fields without changing the major version. Integrations must ignore unknown fields and must not assume object key order. Removing or changing the meaning/type of an existing field requires a new major API path.

The machine-readable service description lives at the v1 root, and the OpenAPI JSON can be checked into an integration's contract tests.

Keep gameplay yours.

Use HaloVault for the physical-card proof boundary, then keep player accounts, matchmaking, decks, rules, and match state in your own platform.

Open the live API root