SealTrustSealTrustSealTrust
Home
How it worksVerifyDemo
Sign inSign up
SealTrust
HomeHow it worksVerifyDemo

Product

Digital Product PassportFeaturesPricingSolutionsRegulation radarUse casesIntegrations

Company

AboutCareersPressContact

Resources

BlogDocumentationAPI DocsDevelopersSecurityTrust CenterAll resources
Sign inCreate an account

© 2026 SealTrust

SealTrust

Product authentication through NFC and blockchain. Protect your brand against counterfeiting.

Product

  • How it works
  • Features
  • Pricing
  • Solutions
  • Regulation radar
  • Use cases
  • Integrations
  • Documentation

Company

  • About
  • Contact
  • Careers
  • Press

Legal

  • Terms of Use
  • Terms of Sale
  • Privacy Policy
  • Legal notices
  • GDPR
  • Cookie Policy
  • Return & warranty

Resources

  • All resources
  • Technical documentation
  • Blog
  • Digital Product Passport
  • DPP 2027 Guide
  • Developers
  • Verification badge
  • Security
  • Trust Center
  • API Docs
  • Help
  • Support

EN 18219 · EN 18220 · ESPR-ready · GDPR

© 2026 SealTrust. All rights reserved.

Follow us on LinkedInMade with trust in France

Developers

The product-proof API

Certificates, passports and on-chain proofs are served by plain REST endpoints, most of them public, because a proof you cannot check yourself is not a proof. Scoped keys, signed webhooks and a typed SDK cover the rest.

https://api.sealtrust.io

Start with a curl

The verification surface requires no account and no API key. Fetch a certificate, a passport, or the Merkle proof of a product and check our anchoring yourself on Base L2 (basescan.org). The proof is public and independently verifiable.

curl
# Public endpoints — no API key required
curl https://api.sealtrust.io/certificate/{identifier}

# The passport as JSON-LD (Schema.org / GS1 vocabulary)
curl "https://api.sealtrust.io/passport/{identifier}?format=jsonld"

# The public Merkle proof — verify our anchoring yourself on Base L2
curl https://api.sealtrust.io/verify/merkle/{identifier}

Public endpoints

Everything below is live and unauthenticated (rate limits apply). An identifier can be the printed serial number, a uid_hash (0x + 64 hex) or a token_id; /certificate, /resolve and /verify/merkle also accept a certificate number.

EndpointWhat it does
GET/sdm/verify-urlVerify an NTAG 424 DNA scan (SDM): decrypts and validates the tag's single-use code.
GET/certificate/{identifier}Public certificate of authenticity (by printed serial, uid_hash, token_id or certificate number).
GET/certificate/{identifier}/downloadThe same certificate as a PDF.
GET/passport/{identifier}Digital Product Passport, filtered by the access profile requested. Add ?format=jsonld for JSON-LD (Schema.org/GS1).
GET/passport/{identifier}/vcThe passport as a tier-filtered SD-JWT-VC verifiable credential.
GET/passport/{identifier}/vc/verifyVerify the stored SD-JWT-VC against the brand's signing key.
GET/brand/{brand_id}/did.jsonBrand DID Document (did:web): public signing keys as JsonWebKey2020.
GET/p/{serial}The unique product identifier (EN 18219). This is what the QR code printed on a product carries, and the only identifier a human can read off an object. Redirects to the product page; add ?linkType=dpp for the passport itself.
GET/01/{gtin}/21/{serial}GS1 Digital Link resolver: the same unit as /p/{serial}, under the GS1 carrier form.
GET/resolve/{identifier}Universal resolver: product + certificate + passport + lifecycle events + media in one response.
GET/passport/{identifier}/proofProof bundle for the passport: SHA-256 data hash, IPFS copy, Base L2 anchor and SD-JWT-VC status.
GET/timeline/{identifier}Combined history: verifications and ownership transfers. Rate-limited to 30 requests per 60 seconds per IP.
GET/verify/merkle/{identifier}Public Merkle anchor proof for an anchored product: recompute it yourself against Base L2.
GET/qr/product/{identifier}QR code (PNG) pointing to the product's verification page.

Partner API: keys & scopes

Write operations use API keys (prefix st_live_) sent as Authorization: Bearer. The full secret is shown once at creation; only its SHA-256 hash is stored. Each key is brand-scoped, quota-limited per day, rate-limited, and carries explicit scopes:

mint:batchBatch mint via /partner/mint/batch
sellout:writeDeclare a sell-out via /partner/sellout
webhooks:readList your webhook subscriptions
webhooks:writeCreate, update and delete webhook subscriptions
mint:singleGrantable, but no endpoint requires it yet
products:readGrantable, but no endpoint requires it yet
products:statusGrantable, but no endpoint requires it yet
transfers:createGrantable, but no endpoint requires it yet

Batch mint over the API

POST /partner/mint/batch accepts JSON (a list of items) or a CSV upload, enforces brand isolation and quotas, supports an Idempotency-Key header for safe retries, and returns a job you can poll.

POST /partner/mint/batch
curl -X POST https://api.sealtrust.io/partner/mint/batch \
  -H "Authorization: Bearer st_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-2027-0042" \
  -d '[
    {
      "product_name": "Sneaker #001",
      "brand_id": 1,
      "category_id": 3,
      "metadata_uri": "https://metadata.sealtrust.io/001.json"
    }
  ]'

# → { "job_id": "abc123...", "status": "queued", "items_count": 1, "brand_id": 1 }
# Poll: GET /partner/mint/batch/status/{job_id}  (max 500 items per batch)

Webhooks, signed

Subscribe a URL per brand and receive events as JSON POSTs. The subscription also accepts six event names we do not send today; subscribing to one of them returns no error and produces no delivery. Every delivery is signed with your webhook secret: X-Webhook-Signature carries t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of "{timestamp}.{raw body}" over the exact bytes we sent. Verify against the raw body, never a re-serialized copy of it, and check the timestamp against a tolerance you choose. Each delivery also carries X-Webhook-Timestamp and X-Webhook-Id, the SHA-256 of the body, identical on every retry of an event so you can deduplicate:

verify_webhook.py
import hashlib
import hmac
import time

def verify_webhook(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    """Verify X-Webhook-Signature: t=<unix_seconds>,v1=<hmac_sha256_hex>."""
    parts = {}
    for item in header.split(","):
        key, _, value = item.strip().partition("=")
        if value:
            parts[key] = value

    ts, sig = parts.get("t"), parts.get("v1")
    if not ts or not sig or not ts.isdigit():
        return False
    if abs(time.time() - int(ts)) > tolerance:   # tolerance is yours to choose
        return False

    signed = ts.encode() + b"." + raw_body       # the signed message, not the body alone
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

# FastAPI example
# raw = await request.body()   # the exact bytes, never a re-serialised dict
# sig = request.headers["X-Webhook-Signature"]
# assert verify_webhook(raw, sig, WEBHOOK_SECRET)

Events we send

product.mintedA product is registered from the console, one at a time. A batch mint over the Partner API does not send it: poll the job status instead.
product.transferredAn ownership transfer carried out by us lands on the chain. A transfer the holder signs from their own wallet does not send it.
transfer.acceptedThe recipient of an escrowed transfer accepts it.
product.scannedA scan lands on one of your products.
product.gray_marketA scan happens outside the area you authorised.
clone.alertWe detect a duplicate on a chip identifier.
return.requestedA return is requested.
return.receivedThe returned product has reached you.
return.completedThe return is settled.
return.rejectedThe return is refused.
return.expiredThe return request expired with no follow-up.
warranty.claimedA warranty is claimed.
buyback.offeredYou offer to buy a product back.
buyback.acceptedThe holder accepts the offer.
buyback.declinedThe holder refuses the offer.
buyback.completedThe buyback is settled.
buyback.expiredThe offer expired with no answer.

TypeScript SDK

@sealtrust-io/sdk is a type-safe client with native fetch and zero dependencies (Node.js ≥ 18). POST and PUT requests automatically carry an Idempotency-Key header; you can provide your own to retry safely.

@sealtrust-io/sdk
import { SealTrustClient } from "@sealtrust-io/sdk";

const sealtrust = new SealTrustClient({
  apiKey: "st_live_...",
  baseUrl: "https://api.sealtrust.io", // optional, this is the default
});

// Read a product's history. The printed serial is the identifier you actually
// have: it is what the QR code on the product carries, and the only one a
// human can read off an object. A token id or a 0x uid hash work too.
const history = await sealtrust.verify.timeline("2MH5NK5F37AE");
console.log(history.product_name);
console.log(history.timeline.length); // verifications + ownership transfers

// Mint a batch, then poll the job
const job = await sealtrust.products.mint([
  { product_name: "Sneaker #001", brand_id: 1, category_id: 3, metadata_uri: "https://metadata.sealtrust.io/001.json" },
]);
const status = await sealtrust.products.getBatchStatus(job.job_id);
console.log(status.status); // "queued" | "started" | "finished" | "failed"

// Subscribe to webhooks
await sealtrust.webhooks.create({
  url: "https://example.com/webhooks/sealtrust",
  events: ["product.minted", "product.transferred"],
  secret: "whsec_...",
});

GS1 Digital Link

Each product identity is addressable through the GS1 Digital Link syntax the ESPR ecosystem converges on: /01/{gtin}/21/{serial} resolves a GTIN + serial to the item's product page, and to its Digital Product Passport under ?linkType=dpp, so the same carrier works for retailers, customs and recyclers without bespoke integrations.

GS1 Digital Link
# One GS1 Digital Link per item: GTIN + serial resolves to the product page
curl https://api.sealtrust.io/01/{gtin}/21/{serial}

# Add ?linkType=dpp to land on the passport itself
curl "https://api.sealtrust.io/01/{gtin}/21/{serial}?linkType=dpp"

# The same identity also resolves certificates and events
curl https://api.sealtrust.io/resolve/{identifier}

Your products, readable by AI agents

@sealtrust-io/mcp-server exposes the public verification surface as a Model Context Protocol (MCP) server: seven read-only tools that any MCP client (Claude Desktop, Claude Code and others) can call to verify a product, read its Digital Product Passport and check the proofs behind it. It runs locally over stdio, needs no account and no API key, and only ever reaches public, unauthenticated read-only endpoints.

Claude Desktop: claude_desktop_config.json
{
  "mcpServers": {
    "sealtrust": {
      "command": "npx",
      "args": ["-y", "@sealtrust-io/mcp-server"]
    }
  }
}
claude mcp add
# Claude Code
claude mcp add sealtrust -- npx -y @sealtrust-io/mcp-server

# Optional: point it at another environment (default: https://api.sealtrust.io)
claude mcp add sealtrust --env SEALTRUST_API_URL=https://api.sealtrust.io -- npx -y @sealtrust-io/mcp-server

The seven tools

verify_productAuthenticity status of a product (authentic, revoked, expired, found without an active certificate, or unknown) plus public product info.
get_passportThe published Digital Product Passport, public tier only (JSON or JSON-LD).
get_passport_proofProof bundle: SHA-256 data hash, IPFS copy, Base L2 anchor, SD-JWT-VC status, physical verification count.
get_certificateThe public certificate of authenticity (status, dates, issuer).
resolve_gs1Resolve a GS1 Digital Link to a passport: the item's with a serial, the model's reference passport without one.
verify_credentialVerify the passport's SD-JWT-VC against the brand's did:web signing key.
get_product_historyThe item's timeline: verifications and ownership transfers, oldest first.

Read-only by design: the server can only GET public data. Nothing it does can mint, transfer or modify a product.

Go further

Full API referenceIntegration guides & documentation

A question about your integration? Talk it through with our team.

Talk to us→