Test Your Integration
Your store has a test store beside it: the same token page, the same token and payout, and a copy of your catalog. Orders on it are paid with one API call instead of a wallet. Nothing is sent on chain, no money moves and no fee is taken. Everything else is the store API you already use: the same orders, items, receipts, events and webhooks, marked as test.
Build your server against the test store, run a purchase end to end as often as you like, then go live by switching the key.
Test mode is available where GET https://api.chaindaddy.io/api/v2/iap/config answers "testMode": true.
1. Create the test store
You need your account credentials and manager role on the token page, as for any store change (Authentication). Pass your live store's id:
chaindaddy iap store test 3f0c… --json
# → {"ok":true,"store":{"id":"7a21…","mode":"test","name":"MYTOKEN (test)","feeBps":0,…}}curl -X POST https://api.chaindaddy.io/api/v2/iap/stores/3f0c…/test-store \
-H "Authorization: Bearer $CHAINDADDY_API_KEY"MCP: iap_open_test_store. The first call creates the test store (201) and every call after that returns it (200). GET on the same path returns it, or 404 before it exists.
The test store starts with a copy of every item in your live catalog as it is at that moment, drafts included, with nothing sold. From then on the two catalogs are separate: change a test item and the live one is untouched, and a new live item does not appear in the test store until you add it there (chaindaddy iap sku create 7a21… --file …).
2. Create a test key
Keys are made on the test store exactly as on your live store (Store keys). A test store's keys start with cd_iap_test_:
chaindaddy iap key create 7a21… --name app-server-test \
--scope orders:read orders:write entitlements:read entitlements:write events:read
# prints the cd_iap_test_… secret ONCE
export CHAINDADDY_IAP_KEY=cd_iap_test_…A test key works only on its test store, and a live key only on its live store.
3. Create an order for a wallet
The same call as a live purchase, with the test key and the test store's id:
curl -X POST https://api.chaindaddy.io/api/v2/iap/orders \
-H "Authorization: Bearer $CHAINDADDY_IAP_KEY" \
-H "Idempotency-Key: test-cart-1" \
-H "Content-Type: application/json" \
-d '{"storeId":"7a21…","sku":"credits.100","quantity":1,"wallet":"0xabc…","appAccountToken":"user-42"}'The order locks its price and deadline as a live one does, with no fee, and its payment asks for nothing to be signed:
{
"id": "0x51c0…",
"storeId": "7a21…",
"sku": "credits.100",
"wallet": "0xabc…",
"amount": "50000000000000000000",
"feeAmount": "0",
"feeBps": 0,
"status": "pending",
"payment": { "kind": "test" }
}4. Pay it
curl -X POST https://api.chaindaddy.io/api/v2/iap/orders/0x51c0…/test-pay \
-H "Authorization: Bearer $CHAINDADDY_IAP_KEY"
# → {"status":"confirmed","txHash":"test:4f0e…","receipt":"eyJhbGciOiJFZERTQSIs…",…}CLI: chaindaddy iap orders test-pay 0x51c0…. MCP: iap_test_pay.
The order is confirmed in the response. It went through the same confirmation a real payment does: the items are granted, the receipt is signed, supply and per-wallet limits count it, and the order.confirmed event is written. Where a transaction hash would be, it carries test: and an id. Calling it again returns the same confirmed order and changes nothing.
It needs a key with orders:write, or your own session as a manager of the token page. It answers 409 LIVE_ORDER for an order on a live store, which is only ever paid on chain, and 409 ORDER_EXPIRED once the order's deadline has passed; create a new order then. POST …/submit answers 409 TEST_ORDER for a test order.
5. Check the receipt
The receipt is signed with the same key as live receipts and verifies the same way (Verifying receipts). Its payload names the test store and says it is a test:
{
"orderId": "0x51c0…",
"storeId": "7a21…",
"feeAmount": "0",
"payment": { "txHash": "test:4f0e…", "logIndex": 0, "blockNumber": 0, "payer": "0xabc…" },
"livemode": false
}Live receipts carry "livemode": true. In production, check both storeId (your live store) and livemode before you hand anything over.
6. Read and spend the items
chaindaddy iap entitlements 7a21… --wallet 0xabc… --json
chaindaddy iap consume 7a21… --wallet 0xabc… --sku credits --qty 1 --idempotency-key job-1These are the same calls as live (Read and spend items). Grants, revokes, refunds, drops you award from your server, the event feed and the store's stats all work on the test store and stay in it.
7. Receive the webhooks
Give the test store its own webhook, pointed at your test endpoint:
chaindaddy iap webhook set 7a21… --url https://staging.example.com/hooks/chaindaddyEvery delivery carries livemode:
{
"id": "iap_evt_2291",
"type": "iap.order.confirmed",
"data": {
"eventId": 2291,
"type": "order.confirmed",
"storeId": "7a21…",
"chainId": "eip155:8453",
"crownId": 8,
"livemode": false,
"wallet": "0xabc…",
"orderId": "0x51c0…",
"payload": { "sku": "credits.100", "status": "confirmed", "txHash": "test:4f0e…", "receipt": "eyJ…", "appAccountToken": "user-42" }
}
}A test store's events go to the test store's webhook and nowhere else: not to your live store's webhook, and not to the iap.* webhooks on your account. Your live store's events never reach the test store's webhook. iap.entitlement.consumed and the rest arrive the same way.
8. Go live
Switch your server to the live store's key. Nothing else in your code changes if you read the store id from the key instead of configuring it: a store key's GET /api/v2/iap/stores returns its own store.
curl https://api.chaindaddy.io/api/v2/iap/stores -H "Authorization: Bearer $CHAINDADDY_IAP_KEY"
# → {"entries":[{"id":"3f0c…","mode":"live",…}]}The test store stays where it is for your next change.
What is different in test mode
| Test store | Live store | |
|---|---|---|
| Paying an order | POST /api/v2/iap/orders/{id}/test-pay | The buyer's wallet, on chain |
payment on a pending order | {"kind":"test"} | The EVM or Solana payment |
| Fee | None (feeBps: 0) | The fee |
| Receipts and webhooks | livemode: false | livemode: true |
| Notifications to you and the buyer | None | As listed |
| Visible on your token page, in the Item showcase and to buyers | No. Only people who manage the token page can read it | Yes |
| Claim links of its drops | Do not open | Open |
| Token prizes | Refused (409 LIVE_STORE_ONLY): they pay real tokens | Yes |
| Messages to customers | None: a test store has no customers | Yes |
An order that is never paid expires at its deadline, as a live one does. An item behind a hold rule still checks the wallet's real balance, and a dollar-priced item converts at the live rate.