Shipping API v1
Ship your customers' vaulted cards from your own servers. Your server calls CollectorCrypt with an API key; your customer never signs in to CollectorCrypt. The only thing your customer signs is the redemption itself, on chain, with their own wallet.
What is different from the Shipping API
Everything you create through v1 lives in your namespace: your API partner account × your customer.
- Addresses and shipments you create are visible only to you. The same wallet does not see them on collectorcrypt.com, and another partner using the same wallet never sees them.
- You never see your customer's collectorcrypt.com addresses, email, phone number or shipments.
- CollectorCrypt sends your customer no email, push or in-app notice about a v1 shipment. Poll v1 for status or subscribe to webhooks, and tell your customer yourself. The carrier's own tracking emails go to the contact email you put on the address.
- You identify customers by your own id (
externalId), in the URL path. There is noX-CC-Customerheader.
If you integrated the Shipping API with an API key, that integration keeps working. v1 is a separate key and a separate set of routes; move over when you are ready and tell us when you have, so we can retire your old key.
Base URLs
| Environment | Base URL |
|---|---|
| Production | https://api.collectorcrypt.com/partner/v1 |
| Devnet | https://dev-api.collectorcrypt.com/partner/v1 |
Devnet uses Solana devnet and the EVM testnets, including Base Sepolia and Robinhood Chain testnet.
Authentication
Send your key as a bearer token on every request:
curl https://api.collectorcrypt.com/partner/v1/customers/cust_123 \
-H 'Authorization: Bearer ccsk_...'
- Your first key is issued by CollectorCrypt. Email support@collectorcrypt.com with the scopes and payment rails you need. After that, you replace and revoke your keys yourself in the partner console.
- A key is a server secret. Never ship it to a browser or a mobile app.
- A v1 key works only on
/partner/v1. It is not accepted anywhere else in the API. - Revocation takes effect on the next request.
Scopes
Each key carries only the scopes it was issued with. A route outside them returns 403 SCOPE_MISSING.
| Scope | Routes |
|---|---|
customers:provision | POST /customers, GET /customers/:externalId |
shipping-address | /customers/:externalId/addresses/* |
redeem | estimate, create, burn, signatures, rebuild |
outbound-shipment | GET /customers/:externalId/shipments[/:id] |
Payment rails
Your account is enabled for one or more rails:
| Rail | Customer | Who pays | Section |
|---|---|---|---|
| Solana | A Solana wallet | The customer, or your own wallet, in Solana USDC inside the burn transaction | Solana redemptions |
| EVM | An EVM wallet | The customer's wallet, on the card's chain (USDC on Base, USDG on Robinhood), with a pay() call next to the burn | EVM redemptions |
The flow
- Register the customer —
POST /customerswith yourexternalIdand their wallet. - Save an address —
POST /customers/:externalId/addresses. - Estimate (optional) —
POST /customers/:externalId/shipments/estimate. - Create the shipment —
POST /customers/:externalId/shipments. Returns the transactions to sign, or the EVM calls to send. - Your customer signs, and you submit what they signed.
- Track —
GET /customers/:externalId/shipments/:iduntil it isPending, thenShippedandDelivered.
How a shipment is proven
A shipment moves forward only on evidence that names it:
- a burn of each of its cards, carrying its shipment id, and
- a payment carrying its shipment id.
A card that was burned for some other order does not count toward yours, even though the card is gone. So always submit, or let CollectorCrypt observe, the transactions that were built for this shipment.
Limits
| Limit | Value |
|---|---|
| EVM cards per chain per shipment | 50 |
Shipments per page (pageSize) | 100 |
| Wallets per customer | 1 (Solana or EVM) |
externalId length | 1–160 characters |
A person with both a Solana and an EVM wallet is two customers in v1, one per wallet. One shipment holds cards from one chain.
Conventions
- JSON in and out. Send
Content-Type: application/jsonand a non-emptyUser-Agent. - Every error has a string
code:{ "statusCode": 404, "code": "CUSTOMER_UNKNOWN", "message": "..." }. Branch oncode, not onmessage. See Errors. - Cost fields are decimal strings in USD.
- Card identifiers are the same
nftAddressvalues the Shipping API uses: the base58 asset address on Solana,<chain>-<contract>-<tokenId>on EVM. See Card identifiers.
Customers
POST /customers — scope customers:provision.
| Field | Type | Required | Notes |
|---|---|---|---|
externalId | string | yes | Your id for this customer, 1–160 characters. It goes in every other URL. |
walletChain | solana | evm | yes | |
wallet | string | yes | Base58 for Solana; 0x… for EVM (any case, stored lowercase). |
curl -X POST https://api.collectorcrypt.com/partner/v1/customers \
-H 'Authorization: Bearer ccsk_...' \
-H 'Content-Type: application/json' \
-d '{"externalId":"cust_123","walletChain":"solana","wallet":"<base58 address>"}'
{ "externalId": "cust_123", "walletChain": "solana", "wallet": "<base58 address>", "created": true, "createdAt": "2026-10-01T12:00:00.000Z" }
- Idempotent. Posting the same body again returns the same customer with
"created": false. - An
externalIdis bound to one wallet for good. Posting it with a different wallet returns409 CUSTOMER_WALLET_CONFLICT. Register the new wallet under a newexternalId. - One wallet, one customer. Registering a wallet you already registered under another
externalIdreturns409 WALLET_ALREADY_REGISTERED. - No proof of ownership is needed, and none is asked of your customer. Registering a wallet gives you nothing over it: you see only what you create, and nothing ships until the wallet itself signs the burn.
- Solana smart-contract wallets are not supported (
400 WALLET_NOT_SUPPORTED). EVM smart wallets, such as Coinbase Smart Wallet, are supported.
GET /customers/:externalId returns the same object, or 404 CUSTOMER_UNKNOWN.
Every route under /customers/:externalId returns 404 CUSTOMER_UNKNOWN for an id you have not registered. Another partner's customer looks exactly like one that does not exist.
Addresses
Addresses belong to your customer in your namespace. Your customer never sees them on collectorcrypt.com, and you never see the ones they saved there.
POST /customers/:externalId/addresses — scope shipping-address.
| Field | Type | Required | Notes |
|---|---|---|---|
fullName | string | yes | ≤ 128 |
streetAddress | string | yes | ≤ 256 |
apartment | string | no | ≤ 128 |
city | string | yes | ≤ 128 |
state | string | yes | A code or a name; stored as the subdivision name, so "CA" becomes "California". Send an empty string for a country with no subdivisions. |
zip | string | no | Needed for correct US rates. ≤ 32 |
country | string | yes | Alpha-2 (US), alpha-3 (USA), or the full name. |
phoneNumber | string | yes | The carrier's contact. ≤ 32 |
email | string | yes | The carrier's contact, and where carrier tracking emails go. Your customer's or your own support address. ≤ 320 |
Phone and email are always required, and they belong to this address only. Nothing is read from, or written to, your customer's collectorcrypt.com account.
{
"id": "cm...", "fullName": "Ada Lovelace", "streetAddress": "1 Test St", "apartment": null,
"city": "Billings", "state": "Montana", "zip": "59102", "country": "United States",
"phoneNumber": "+14065550100", "email": "ada@example.com", "createdAt": "2026-10-01T12:00:00.000Z"
}
Keep id; it is the addressId you ship to.
| Route | Notes |
|---|---|
GET /customers/:externalId/addresses | { "items": [ … ] }, newest first. |
GET /customers/:externalId/addresses/:id | One address, or 404 ADDRESS_NOT_FOUND. |
PATCH /customers/:externalId/addresses/:id | Any subset of the fields above. The result must still have a phone and an email. |
DELETE /customers/:externalId/addresses/:id | 204. The address can no longer be used for new shipments. |
A shipment's address is frozen when it is created: editing or deleting the address later does not change where an existing shipment goes.
Errors: 400 CONTACT_PHONE_REQUIRED · 400 CONTACT_EMAIL_REQUIRED · 400 DESTINATION_RESTRICTED · 400 VALIDATION_FAILED · 404 ADDRESS_NOT_FOUND.
Estimate
POST /customers/:externalId/shipments/estimate — scope redeem. Optional, for every rail.
| Field | Type | Required |
|---|---|---|
nftAddresses | string[] | yes, at least 1 |
addressId | string | yes |
payCustomsDuties | boolean | no |
The response is the same as the Shipping API's estimate: price (declared value, not a charge), shippingPrice, insurancePrice, feesPrice, total, customsDutiesEstimate, breakdown. What your customer pays is total. Render breakdown.lines[] generically.
For an EVM customer, estimate refuses the same card sets create does: 400 MIXED_CHAINS and 400 CHAIN_NOT_SUPPORTED. See EVM redemptions.
Solana redemptions
For a customer registered with a Solana wallet. CollectorCrypt builds the transactions and signs them as fee payer; your customer signs them as the card owner; you submit them. The shipping fee rides inside the first burn transaction, so the burn and the payment land together.
Create a Solana shipment
POST /customers/:externalId/shipments — scope redeem.
| Field | Type | Required | Notes |
|---|---|---|---|
nftAddresses | string[] | yes | Cards held by the customer's Solana wallet. |
addressId | string | yes | |
payCustomsDuties | boolean | no | |
payerWallet | string | no | A Solana wallet of yours that pays the fee instead of your customer. It must sign too — see Paying for your customer. |
{
"outboundShipmentId": "cm...",
"reused": false,
"rail": "solana",
"totalCost": "5.99",
"amountDue": "5.99",
"breakdown": { },
"transactions": ["<base64>"],
"delistTransactions": []
}
transactions— have your customer's wallet sign every entry, then submit them. The fee is in the first one.delistTransactions— usually[]. Non-empty when a card is listed on an external marketplace and must be released first. Sign and submit these too.reused— posting an identical body again returns the same shipment with fresh transactions. Changing the address,payCustomsDutiesorpayerWalletcreates a new shipment.totalCostis the order's total;amountDueis what is still owed. Both are decimal strings.
A Solana transaction carries a recent blockhash and expires roughly 60–90 seconds after it was built. If yours expires, call rebuild; do not create a new shipment.
Submit through CollectorCrypt
POST /customers/:externalId/shipments/:id/burn — scope redeem.
{ "transactions": ["<base64 signed>"], "delistTransactions": [] }
CollectorCrypt checks that these are exactly the transactions it built for this shipment, broadcasts them and waits for confirmation. The response is an array, one element per transaction:
[{ "error": null, "transactionId": "<signature>", "transactionUrl": "https://..." }]
Inspect every element: a non-null error means that transaction did not land. Transactions CollectorCrypt did not build for this shipment are refused with 403 BATCH_NOT_ISSUED.
Report transactions your customer's wallet sent
Many wallets sign and send in one step. That is fine; report what landed.
POST /customers/:externalId/shipments/:id/signatures — scope redeem.
{ "signatures": ["<signature>", "<signature>"] }
[
{ "signature": "<signature>", "status": "linked" },
{ "signature": "<signature>", "status": "pending" }
]
status | Meaning |
|---|---|
linked | Confirmed, verified as this shipment's, and recorded. |
pending | Not confirmed yet. Report it again in a few seconds. |
rejected | Not a transaction CollectorCrypt built for this shipment. reason says why. |
A transaction is accepted only if CollectorCrypt's own key paid its network fee and its memo names this shipment. Only CollectorCrypt can produce that signature, which proves the landed transaction is the one built for this order. Reporting the same signature twice is safe.
Report the burn transactions. A delistTransactions signature needs no report; if you include one it comes back rejected with reason not a burn transaction for this shipment, and the rest of the report is still processed.
Use this route too if a burn call timed out: report the signatures and the shipment catches up.
Rebuild expired transactions
POST /customers/:externalId/shipments/:id/rebuild — scope redeem.
Returns fresh transactions and delistTransactions for the cards on this shipment that have not burned yet, in the same shape as create, plus feeReCollected (whether the fee is in this batch) and remainingCards. Before building, CollectorCrypt looks for transactions of this shipment that your customer's wallet already sent, so a fee that landed is never charged again. Use it when the blockhash expired, after some transactions of a multi-transaction shipment failed, or after a batch is refused with BATCH_NOT_ISSUED because it is older than 15 minutes. Solana only.
Paying for your customer
Send payerWallet on create. The fee instruction in the first transaction is then paid from that wallet, so that transaction needs two signatures: your payer's and your customer's. CollectorCrypt pays the network fee either way. payerWallet is Solana-only.
Errors: 400 CARD_NOT_REDEEMABLE · 404 CARDS_NOT_FOUND · 400 MIXED_CHAINS · 400 INSUFFICIENT_BALANCE · 403 RAIL_NOT_ENABLED · 403 BATCH_NOT_ISSUED · 403 BATCH_INCOMPLETE · 409 DELIST_FAILED · 409 SHIPMENT_NOT_MODIFIABLE.
EVM redemptions
For a customer registered with an EVM wallet. The customer's wallet sends the burn and the payment itself; CollectorCrypt signs nothing.
Payment is on the card's chain: USDC on Base, USDG on Robinhood. One shipment holds cards from one chain (MIXED_CHAINS), so the burn and the payment go to the same chain. A card on any other EVM chain is refused with CHAIN_NOT_SUPPORTED.
| Chain | Token | Production | Devnet |
|---|---|---|---|
| Base | USDC | 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 | 0x036cbd53842c5426634e7929541ec2318f3dcf7e |
| Robinhood Chain | USDG | 0x5fc5360d0400a0fd4f2af552add042d716f1d168 | 0x7e955252e15c84f5768b83c41a71f9eba181802f |
The create response names the token in payment.token. Both tokens have 6 decimals.
Create an EVM shipment
POST /customers/:externalId/shipments — scope redeem. Body: nftAddresses, addressId, payCustomsDuties. At most 50 cards per chain per shipment.
{
"outboundShipmentId": "cm...",
"reused": false,
"rail": "evm",
"totalCost": "5.99",
"amountDue": "5.99",
"breakdown": { },
"evmCallBundle": {
"chainId": 8453,
"atomicPreferred": true,
"calls": [
{ "kind": "approve", "to": "0x...", "value": "0", "data": "0x..." },
{ "kind": "batchBurn", "to": "0x...", "value": "0", "data": "0x..." },
{ "kind": "pay", "to": "0x...", "value": "0", "data": "0x..." }
],
"payment": {
"chain": "base", "chainId": 8453, "payer": "0x...", "token": "0x...", "spender": "0x...",
"amountRaw": "5990000", "memo": "cm...", "amountDue": "5.99"
}
},
"payment": { },
"evmTransactions": [
{ "chain": "base", "chainId": 8453, "tokenIds": ["492"], "owner": "0x...",
"burnTxs": [{ "to": "0x...", "data": "0x...", "value": "0", "chainId": 8453 }] }
]
}
batchBurncarries the shipment id as its memo, and so doespay(payment.memo). That is what ties both to this shipment.approvegrants exactly the amount due, never an unlimited allowance.amountRawis in the token's 6-decimal units (5990000is 5.99). The amount is fixed when the shipment is created.chainIdis the cards' chain. The example is a Base card; a Robinhood card returns"chain": "robinhood",chainId4663(devnet46630), and USDG astoken.paymentrepeatsevmCallBundle.payment.
Send the calls
Send from payment.payer, the wallet holding the cards, on evmCallBundle.chainId. Every call in the bundle goes to that one chain; a wallet connected to another chain must switch first.
- Wallets with EIP-5792, such as Coinbase Smart Wallet: send
evmCallBundle.callsas onewallet_sendCallsbatch. The burn and the payment then succeed or fail together. - Other wallets: send each call in order with
eth_sendTransaction, waiting for each receipt. The burn comes beforepay; if it reverts, stop — nothing has been charged.
Report the burn
Optional. CollectorCrypt reads the burn and the Paid event from the chain and matches both to the shipment by memo. To record the burn sooner, send the hash:
POST /customers/:externalId/shipments/:id/burn — scope redeem.
{ "evmTransactions": [{ "chain": "base", "txHash": "0x..." }] }
[{ "txHash": "0x...", "status": "confirmed" }]
status | Meaning |
|---|---|
confirmed | Mined, and it burned this shipment's cards with this shipment's memo. Recorded. |
pending | Not mined within about 20 seconds. Nothing was recorded. Report it again, or let CollectorCrypt pick it up. |
rejected | Reverted, or not a burn for this shipment. reason says why. |
A hash is only ever recorded after it is verified on chain, so reporting early or reporting the wrong hash cannot mark anything.
On this rail, PaymentPending means the burn is recorded and no payment has been seen yet: the approve and pay calls still apply, so send them. The payment is credited after enough confirmations, usually within about 5 minutes.
Errors: 400 CARD_NOT_REDEEMABLE · 404 CARDS_NOT_FOUND · 400 MIXED_CHAINS · 400 TOO_MANY_EVM_CARDS · 400 CHAIN_NOT_SUPPORTED (the cards' chain has no payment token) · 403 RAIL_NOT_ENABLED.
Track shipments
CollectorCrypt does not email or notify your customer about v1 shipments. Poll these routes or subscribe to webhooks, and keep your customer informed yourself. The carrier's own tracking emails go to the email on the shipment's address.
One shipment
GET /customers/:externalId/shipments/:id — scope outbound-shipment. Returns the shipment, or 404 SHIPMENT_NOT_FOUND.
{
"id": "cm...", "customId": "2026100100OS123", "status": "Shipped",
"numberOfCards": "1", "nftAddresses": ["..."], "addressId": "cm...",
"shippingCost": "5.99", "insuranceCost": "0.00", "feesCost": "0.00", "totalCost": "5.99",
"dutiesPrepaid": false, "dutiesPrepaidAmount": null,
"paymentMethod": "crypto", "paymentRail": "solana", "paymentConfirmedAt": "2026-10-01T12:05:00.000Z",
"trackingIds": ["1Z..."], "trackingUrls": ["https://..."],
"payment": null,
"createdAt": "2026-10-01T12:00:00.000Z", "updatedAt": "2026-10-02T09:00:00.000Z"
}
- Cost fields and
numberOfCardsare strings. paymentis the frozen EVM payment intent (chain,token,spender,amountRaw,memo,paid) on the EVM rail, andnullotherwise. It lets you recover what is owed if you lost the create response.customIdis the order number CollectorCrypt's warehouse and support use.
List shipments
GET /customers/:externalId/shipments — scope outbound-shipment.
| Query | Notes |
|---|---|
status | Comma-separated statuses, e.g. Pending,Processing,Shipped. |
createdFrom, createdTo | ISO 8601. |
page | From 1. |
pageSize | Up to 100. |
{ "items": [ ], "page": 1, "pageSize": 50, "total": 3 }
Newest first.
Statuses
| Status | Meaning |
|---|---|
Created | Created; not yet paid or burned. |
PaymentPending | Burn recorded, payment not yet seen (EVM). |
PaymentReceived | Paid, burn not yet recorded. |
Pending | Paid and burned. Accepted by the warehouse. |
Processing | Being packed. |
Shipped | Handed to the carrier. trackingIds are set. |
Delivered | Delivered. |
ActionRequired | Needs CollectorCrypt's attention — email support with the customId. |
Cancelled | Cancelled. |
Treat these as a set, not a sequence: a shipment can skip states, and it never moves backwards.
Webhooks
CollectorCrypt can POST to your server when one of your v1 shipments changes. Webhooks do not replace the API: the GET routes above remain the source of truth.
Endpoint
You have one endpoint per API partner account, set in the partner console. The URL must:
- use
https; - point to a public address. Private, loopback, link-local and cloud-metadata addresses are refused when you save the URL, and again at every connection;
- be at most 2048 characters;
- contain no credentials (
https://user:pass@…is refused).
While your webhook is disabled, changes are not queued. Disabling it also drops any event still waiting for a retry, and enabling it again does not replay them. List your shipments to catch up.
Events
type | Sent when |
|---|---|
shipment.updated | A v1 shipment's status or trackingIds change. |
ping | You send a test from the partner console. data is {}. Sent once, never retried; the console shows the result. |
Request
POST to your URL, with these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | CollectorCrypt-Webhooks/1 |
CC-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256>. See Verify the signature. |
{
"id": "3f0c2a9e-5b7d-4c1e-9a8f-6d2b1e0c7a43.2",
"type": "shipment.updated",
"createdAt": "2026-10-02T12:00:00.000Z",
"data": {
"customerExternalId": "cust_123",
"shipment": { }
}
}
data.shipmentis exactly the objectGET /customers/:externalId/shipments/:idreturns.data.customerExternalIdis theexternalIdyou registered the customer under.ididentifies the event. Treat it as an opaque string.createdAtis when this request was sent, not when the shipment changed.
Current state, at least once
- The payload is the shipment's current state when it is sent, not a diff. Several quick changes can arrive as one event.
- Delivery is at-least-once, and events can arrive out of order. Use
data.shipment.updatedAtto ignore a state older than the one you hold, andidto de-duplicate. - A redelivery of the same state reuses the same
id. A later change gets a newid.
Verify the signature
- Take the raw request body bytes. Verify before you parse the JSON: a re-serialized body may not match.
- Split
CC-Signatureintotandv1. - Compute
HMAC-SHA256(secret, "<t>.<raw body>")as lowercase hex. The key is your whole webhook secret,ccwh_prefix included. - Compare it with
v1in constant time. - Reject the request if
tis more than 5 minutes from your clock.
The secret is ccwh_ followed by base58. To get the raw body: express.raw({ type: 'application/json' }) in Express, request.get_data() in Flask, request.body in Django.
Node.js:
import { createHmac, timingSafeEqual } from 'node:crypto'
const TOLERANCE_S = 300
export function verifyWebhook(rawBody, header, secret) {
const parts = Object.fromEntries(
(header ?? '').split(',').map((p) => p.trim().split('='))
)
const { t, v1 } = parts
if (!/^\d+$/.test(t ?? '') || !v1) return false
if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCE_S) return false
const expected = Buffer.from(
createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex')
)
const given = Buffer.from(v1)
return given.length === expected.length && timingSafeEqual(given, expected)
}
Python:
import hashlib
import hmac
import time
TOLERANCE_S = 300
def verify_webhook(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.strip().split("=", 1) for p in (header or "").split(",") if "=" in p)
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or not v1:
return False
if abs(time.time() - int(t)) > TOLERANCE_S:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
Responses and retries
Any 2xx within 10 seconds is success. Your response body is ignored.
Anything else is a failure: another status, a timeout, a connection error, or a redirect. Redirects are never followed. A failed event is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h: 8 attempts over about 45 hours. After that the event is dropped. A further change to the same shipment queues a fresh event.
Respond quickly and process the event asynchronously.
Signing secret
The secret is shown once: when you create the webhook, and when you roll it. Rolling takes effect immediately: the next delivery is signed with the new secret, and the old one no longer verifies. A delivery you reject while you deploy the new secret is retried on the schedule above.
Partner console
Manage your keys and webhook, and see your numbers.
| Environment | URL |
|---|---|
| Production | partner.collectorcrypt.com |
| Devnet | dev-partner.collectorcrypt.com |
Sign in
Sign in with a regular CollectorCrypt account, the same login as collectorcrypt.com: email, Google or wallet. Until the account is linked to your partner account, the console shows its wallet address. Send that address to support@collectorcrypt.com and we link it. An account belongs to one partner only. To remove someone's access, ask CollectorCrypt; it takes effect on their next request.
Keys
The list shows each key's prefix, name, scopes, status and when it was last used. The console never shows an existing key's secret.
- Replace mints a new key with the same scopes and expiry, and shows its secret once. The old key keeps working until you revoke it, so deploy the new key first, then revoke the old one.
- Revoke takes effect on the next request.
- You cannot change a key's scopes, or replace a revoked or expired key. Ask CollectorCrypt.
A key is still a server secret. Put a new one straight into your server's secret store; the console cannot show it again.
Webhook
Set the URL, enable or disable it, roll the signing secret, send a test ping, see the result of the last delivery, or remove the webhook. Removing it drops any queued events. See Webhooks.
Stats
Your number of customers, your shipments by status, and the shipments created in the last 30 days.
Errors
Every error body has the same three fields:
{ "statusCode": 404, "code": "CUSTOMER_UNKNOWN", "message": "No customer with that externalId" }
Branch on code. message is for humans and may change.
| Status | code | Meaning |
|---|---|---|
| 400 | VALIDATION_FAILED | The body is malformed, a field is missing or invalid, or the route does not apply to this customer (for example signatures for an EVM customer). |
| 400 | CONTACT_PHONE_REQUIRED | The address has no phone number. |
| 400 | CONTACT_EMAIL_REQUIRED | The address has no email. |
| 400 | DESTINATION_RESTRICTED | We do not ship to that country or region. |
| 400 | WALLET_NOT_SUPPORTED | A wallet that cannot be registered: a Solana smart-contract wallet, or a CollectorCrypt system wallet. |
| 400 | CARD_NOT_REDEEMABLE | A card is not in a redeemable state (for example already burned). |
| 400 | MIXED_CHAINS | Cards from more than one chain in one shipment. |
| 400 | TOO_MANY_EVM_CARDS | More than 50 EVM cards on one chain. |
| 400 | CHAIN_NOT_SUPPORTED | The cards' chain has no EVM payment token configured. EVM payment is taken on Base and Robinhood Chain. |
| 400 | INSUFFICIENT_BALANCE | The paying wallet cannot cover the fee. |
| 401 | UNAUTHORIZED | Key missing, invalid, revoked or expired. |
| 403 | SCOPE_MISSING | The key does not carry this route's scope. |
| 403 | RAIL_NOT_ENABLED | Your account is not enabled for this payment rail. |
| 403 | BATCH_NOT_ISSUED | Solana: these are not the transactions CollectorCrypt built for this shipment, they mix two batches, or the batch is older than 15 minutes. Call rebuild. |
| 403 | BATCH_INCOMPLETE | Solana: a transaction is missing or duplicated. |
| 404 | CUSTOMER_UNKNOWN | No such customer in your namespace. |
| 404 | ADDRESS_NOT_FOUND | No such address for this customer. |
| 404 | SHIPMENT_NOT_FOUND | No such shipment for this customer. |
| 404 | ROUTE_NOT_FOUND | No such route under /partner/v1. Check the method and path. |
| 404 | CARDS_NOT_FOUND | A card does not exist, or this customer does not hold it. |
| 409 | CUSTOMER_WALLET_CONFLICT | This externalId is registered to a different wallet. |
| 409 | WALLET_ALREADY_REGISTERED | This wallet is registered under another of your externalIds. |
| 409 | WALLET_AMBIGUOUS | The EVM wallet matches more than one CollectorCrypt account. Contact support. |
| 409 | CUSTOMER_WALLET_UNLINKED | The account this customer was linked to no longer holds the wallet. Contact support. |
| 409 | SHIPMENT_NOT_MODIFIABLE | The shipment is past the state this action needs, or cancelled. |
| 409 | DELIST_FAILED | Solana: releasing a card from an external marketplace listing failed. Nothing was burned. |
| 500 | INTERNAL | Unexpected error. Retry; contact support if it persists. |
| 503 | UNAVAILABLE | Temporarily unavailable. Retry with backoff. |
Anything you did not create looks like it does not exist: another partner's customer, address or shipment returns the same 404 as a random id.
Need help? support@collectorcrypt.com