Grading Company Integration Guide
Everything you need to integrate with the digital-card-twin Protocol and issue verifiable NFT certificates for every graded TCG card.
Architecture
Overview
The digital-card-twin Protocol creates an immutable, on-chain record for every grading certificate issued. When a card is graded:
- The grading company notifies digital-card-twin via webhook or API call.
- The oracle fetches certificate details, pins metadata to IPFS, and submits a signed transaction to Base (Ethereum L2).
- An ERC-721 NFT is minted to the card owner's wallet — the permanent, fraud-proof record.
Grading company
│
│ POST /api/v1/webhooks/psa (webhook push)
│ — or —
│ POST /api/v1/certs/batch-submit (manual / bulk)
▼
digital-card-twin API
│
│ Lookup cert from PSA API
│ Upload metadata + image to IPFS
│ Sign certificate with KMS oracle key (EIP-712)
▼
GradingOracleAdapter.sol (Base)
│
│ Verify EIP-712 signature
│ Call DigitalCardTwinRegistry.mintCertificate()
▼
ERC-721 NFT minted to recipient walletSetup
Getting started
1. Register as a grading company
Before any certificates can be minted, your company must be registered in GradingCompanyRegistry.sol. Contact the digital-card-twin team to complete this one-time on-chain registration. You will receive:
- ▸Your
gradingCompanyId— abytes32value (keccak256of your company name) - ▸Your oracle adapter contract address on Base
- ▸Your API key for the digital-card-twin HTTP API
2. Obtain an API key
API keys are scoped per grading company and authorise write operations. Contact engineering@digital-card-twin.com to request a key for staging and production. Include the key in all write requests:
Authorization: Bearer <your-api-key>
Options
Integration paths
| Path | Best for | Latency |
|---|---|---|
| Webhook (push) | Automated flow — events pushed as cards are graded | Near real-time |
| Batch submit (pull) | Manual backfill, bulk migration, or when webhook is unavailable | On-demand |
Recommended
Path 1 — Webhook integration
Step 1 — Configure the webhook endpoint
| Environment | Webhook URL |
|---|---|
| Staging | https://api-staging.digital-card-twin.com/api/v1/webhooks/psa |
| Production | https://api.digital-card-twin.com/api/v1/webhooks/psa |
Step 2 — Sign the request (HMAC)
All webhook requests must be signed with the shared PSA_WEBHOOK_SECRET:
# Python
import hmac, hashlib, json
secret = b"your-webhook-secret"
body = json.dumps(payload, separators=(",", ":")).encode()
sig = "sha256=" + hmac.new(secret, body, hashlib.sha256).hexdigest()
# Include as header: X-PSA-Signature: {sig}// TypeScript
import { createHmac } from "node:crypto";
const sig = "sha256=" + createHmac("sha256", secret)
.update(JSON.stringify(payload))
.digest("hex");Step 3 — Send a webhook event
POST /api/v1/webhooks/psa
Content-Type: application/json
X-PSA-Signature: sha256=abc123...
{
"event": "grade.issued",
"certNumber": "85237194",
"recipient": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
}| Event | Action |
|---|---|
| grade.issued | Triggers a digital-card-twin mint |
| grade.updated | Accepted, no action in v1 (re-mint support in v2) |
| grade.voided | Accepted, no action in v1 (burn support in v2) |
Response: 202 Accepted
{
"jobId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
"status": "queued",
"certNumber": "85237194"
}Step 4 — Track the mint job
GET /api/v1/jobs/3f2504e0-4f89-11d3-9a0c-0305e82c3301 Authorization: Bearer <your-api-key>
{
"jobId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
"certNumber": "85237194",
"status": "confirmed",
"txHash": "0x4a5c89f2...",
"tokenId": 42,
"blockNumber": 14523901,
"updatedAt": "2026-04-10T09:12:04.123Z"
}Polling strategy: exponential back-off starting at 2 s, capped at 30 s. Typical Base confirmation is 2–4 seconds.
Manual / bulk
Path 2 — Batch submission
POST /api/v1/certs/batch-submit
Content-Type: application/json
Authorization: Bearer <your-api-key>
{
"certs": [
{
"certNumber": "85237194",
"recipient": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
},
{
"certNumber": "72461038",
"recipient": "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"
}
]
}- •Maximum 50 certs per request.
- •Submissions are idempotent — the same
certNumbersubmitted twice will not create two NFTs.
| Status | Meaning |
|---|---|
| queued | Mint job enqueued successfully |
| already_minted | NFT already exists — idempotent, no duplicate minted |
| not_found | Cert number not found in PSA |
| pending_grade | Card graded as AUTH / Pending — not yet a numeric grade |
| error | PSA lookup or metadata pipeline failure — check error field |
Public endpoint
Certificate verification
Anyone can verify whether a cert has been minted — no API key required.
GET /api/v1/certs/85237194/status
{
"certNumber": "85237194",
"status": "minted",
"tokenId": 42,
"txHash": "0x4a5c89f2...",
"blockNumber": 14523901,
"metadataURI": "ipfs://QmXyz.../metadata.json",
"mintedAt": "2026-04-10T09:12:04.000Z"
}Reliability
Error codes & retry guidance
| HTTP | Code | Meaning | Action |
|---|---|---|---|
| 401 | INVALID_SIGNATURE | HMAC mismatch | Re-check secret; regenerate signature |
| 400 | (validation) | Missing required field | Fix request body |
| 422 | PENDING_GRADE | Card not yet assigned a numeric grade | Retry after grading completes |
| 422 | CERT_NOT_FOUND | Cert number not in PSA | Verify cert number is correct |
| 429 | (rate limit) | Rate limit exceeded | Back off and retry (Retry-After header present) |
| 500 | (internal) | Unexpected error | Contact support with jobId |
Webhook retry policy: exponential back-off at 10 s → 30 s → 2 min → 10 min → 1 hour. After 5 failures the digital-card-twin team is alerted.
Oracle retries: up to 5 automatic retries with exponential back-off (base 2 s). After 5 failures the job enters failed state.
Developer experience
TypeScript SDK
npm install @digital-card-twin/sdk
import { DigitalCardTwinClient } from "@digital-card-twin/sdk";
const client = new DigitalCardTwinClient({
apiUrl: "https://api.digital-card-twin.com",
subgraphUrl: "https://api.studio.thegraph.com/query/<ID>/...",
rpcUrl: "https://base-sepolia.g.alchemy.com/v2/<KEY>",
});
// Look up a certificate
const result = await client.lookupCertificate("85237194", "PSA");
if (result.found) {
console.log(result.tokenId, result.owner);
}
// Submit a grading event (requires API key)
const job = await client.submitCertificate(cert, process.env.DCT_API_KEY);
if (job.queued) {
console.log("Job ID:", job.jobId);
}Full documentation: @digital-card-twin/sdk on npm ↗
Network
On-chain contract addresses
| Contract | Base Sepolia (staging) | Base Mainnet |
|---|---|---|
| DigitalCardTwinRegistry | 0xad3898d76af2024ee1ebd2f5ff10fdeefe64361b | TBD |
| PSA GradingOracleAdapter | 0x88caa4def2ada553b44e59d633208474447886d1 | TBD |
Contract ABIs and deployment details are in the contracts/ directory of the monorepo. Registry contracts are verified on Basescan.
Links
Additional resources
API documentation →
Interactive Swagger UI — explore every endpoint directly in your browser.
TypeScript SDK on npm ↗
Install @digital-card-twin/sdk and start submitting certificates in minutes.
Partner portal →
Request early access and review your integration status.
Contact engineering →
engineering@digital-card-twin.com — for API keys, staging access, and support.