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.
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
| Layer | Positive fact | Explicit limit |
|---|---|---|
| Passport | A stable identity record exists for one physical card and its metadata commitment has not silently changed. | Identity is not game legality. |
| Mint / HaloCert | The sealed record recomputes correctly on HaloVault's append-only hash chain and is not revoked. | A self mint is not third-party authentication. |
| Store-Verified | An 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 binding | When 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 controller | A 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. |
| NFC | An 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"
}
}
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:
- Require HTTP 200 and
found=true. - Require
passport.active=trueandmint.valid=true. - Map the
cardidentity to your own catalog. Reject ambiguous or unknown printings. - Require the authentication level appropriate for your queue—for example,
store_verifiedfor ranked physical-card play. - If your experience is token-gated, require
onchain.verified_binding=true. - Require a suitable
token_control.verification. Treatrecorded,unverified,stale,error, andunsupportedaccording to your risk policy. - Issue your own single-use, expiring wallet challenge and compare its recovered signer to
token_control.address. - 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.
- Create a cryptographically random, single-use nonce on your server.
- Bind it to your user/session, requested Passport ID, domain, purpose, chain, issue time, and a short expiration.
- Ask the wallet to sign an unambiguous message. EIP-712 typed data is preferred on EVM; a well-formed EIP-191 message is acceptable.
- Recover the signer server-side and compare normalized addresses with
token_control.address. - 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
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.
- Create or Store-Verify the physical card and confirm its cross-TCG identity before token minting.
- Fetch
/passports/{passport_id}/manifest/. - Use that exact HTTPS URL as the ERC-721
tokenURI. It includes the Passport's sealed commitment and HaloCert record hash. - Mint to the player's non-custodial wallet through the provider or contract of your choice.
- From the authenticated collector/store workflow, record the CAIP-2 chain, contract, token ID, and mint transaction hash.
- After confirmation, run the chain sync. On a configured EVM chain, HaloVault calls
ownerOf(tokenId)andtokenURI(tokenId). A matching canonical manifest changes the binding fromrecordedtoverified.
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.
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
- Public responses use
Access-Control-Allow-Origin: *and never use cookies or credentials. - Single lookups default to 120 requests per IP per 60 seconds.
- Manifests default to 180 requests per IP per 60 seconds.
- Batch calls default to 30 requests per IP per 60 seconds, with at most 50 queries per call.
- Batch bodies are limited to 64 KiB.
- Lookups are cacheable for 30 seconds; manifests and discovery documents are cacheable for 5 minutes.
- A
429includesRetry-After. Honor it and use exponential backoff.
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
| HTTP | Error | Meaning |
|---|---|---|
| 400 | one_lookup_mode_required | The resolve query mixed identifiers or supplied none. |
| 400 | incomplete_chain_lookup | A chain tuple is missing required fields. |
| 400 | queries_required | The batch body did not contain a non-empty array. |
| 400 | batch_too_large | More than 50 queries were submitted. |
| 404 | passport_not_found | No public Passport matches the identifier. |
| 405 | method_not_allowed | The endpoint does not support that HTTP method. |
| 413 | request_too_large | The batch body exceeded 64 KiB. |
| 429 | rate_limited | Wait 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
- The public API exposes public card identity, certificate facts, token references, and public blockchain addresses. It does not expose vault location, purchase cost, private notes, account email, or private media.
- Never infer physical custody from a wallet signature. Pair higher-risk actions with fresh physical evidence or a store workflow when appropriate.
- Never infer legal ownership from a token transfer. Disputes, theft, custody arrangements, and jurisdictional title rules exist outside blockchain state.
- Require HTTPS and validate the hostname before following a URL. The canonical manifest is on
halovault.ai. - Treat catalog text as data, not HTML. Escape it in your interface.
- Do not make a public API call the sole availability dependency for an in-progress match. Cache the admission decision for that match and define a safe outage policy.
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
HaloVault