Get started · 2 of 15
Sandbox quickstart
Push a listing, see it in browse, receive an order, and report fulfillment in the sandbox.
By the end, your sandbox seller has a listing buyers can find, a test order reported shipped, and that order’s test-mode payout.
Every request uses https://api-sandbox.snapcaster.ca/api/v1/marketplace and comes from the published API contract, so you can copy it as written.
Before you start
Section titled “Before you start”You need these from Credentials and onboarding and the 1Password share Snapcaster sends you:
- The sandbox seller’s marketplace API key, set in your shell as
MARKETPLACE_API_KEY, such asexport MARKETPLACE_API_KEY='mp_yourSellerCode_yourSecret'. - A webhook receiver that verifies signatures with your platform’s sandbox signing secret, registered before the test purchase so the purchase’s events reach you. Build a webhook receiver gives you one to run, and Test your receiver checks it.
skuId14602 andproductId86 from the sandbox catalog SKUs, which Snapcaster also sends with your sandbox credentials.
Steps 5 and 6 report and read the order through the API, which needs an orders or both subscription, and step 1 shows yours in subscription.
On Path A, those requests return HTTP 403 SUBSCRIPTION_EXCLUDES_ORDERS.
Instead, confirm that your receiver applies the test purchase’s inventory.adjusted.
The seller then ships the order through the account API with the requests in Ship a Path A test order.
Step 1: verify your credential
Check that your API key belongs to the expected sandbox seller.
Set its sellerCode in your shell, for example export SELLER_CODE=yourSellerCode, and note counts.browsable for step 3.
Request
curl --request GET \
--url 'https://api-sandbox.snapcaster.ca/api/v1/marketplace/me' \
--header "Authorization: Bearer $MARKETPLACE_API_KEY"{
"success": true,
"data": {
"sellerId": 42,
"sellerCode": "sandbox-cards",
"storeName": "Sandbox Cards",
"currency": "CAD",
"region": "CA",
"subscription": "orders",
"active": true,
"vacation": false,
"counts": {
"total": 1,
"matched": 1,
"unmatched": 0,
"browsable": 1
},
"readiness": {
"browseEligible": true,
"effectiveTermsVersion": "2026-10-27",
"incompletePrerequisites": []
}
}
}Expected result
A 200 response identifies your sandbox store and reports your platform's subscription, CAD, active true, vacation false, and the current listing counts.
Troubleshooting
For 401 UNAUTHORIZED, check that you copied the complete marketplace API key for this seller. If the key was revoked, replace it.
Step 2: push a first listing
Push one listing with the supplied test SKU and a price in integer cents.
The request sets asOf to the current UTC time, but in your integration, send the time you measured stock.
To push a whole catalog, follow Sync a full catalog.
Request
curl --request POST \
--url 'https://api-sandbox.snapcaster.ca/api/v1/marketplace/listings' \
--header "Authorization: Bearer $MARKETPLACE_API_KEY" \
--header 'Content-Type: application/json' \
--data @- <<EOF
{
"skuId": 14602,
"productId": 86,
"price": 2499,
"quantity": 3,
"externalRef": "platform-listing-1042",
"asOf": "$(node -p 'new Date().toISOString()')"
}
EOF$(node -p 'new Date().toISOString()') sets asOf to the current UTC time in milliseconds, so two writes in the same second never share an asOf.
{
"success": true,
"data": {
"results": [
{
"skuId": 14602,
"op": "upsert",
"status": "applied",
"externalRef": "platform-listing-1042"
}
]
}
}Expected result
The per-item result is applied, and a follow-up GET /me counts the new listing in total and matched.
Troubleshooting
For an unmatched result, use a skuId and productId from the sandbox catalog SKUs on the Sandbox page. For a rejected result, correct the field named in the response. For PRODUCT_MISMATCH, send the productId the reason names, or omit productId.
Step 3: confirm it is browsable
Read GET /me and compare counts.browsable with the count you noted in step 1.
It rises by one once the seller is browse-eligible and the new listing is matched and in stock.
Then read the product as a buyer would, every few seconds, until your listing appears in offers.
Browse lags a listing write by a few seconds, so the first read can miss your listing even after the count has risen.
The product read returns the 24 cheapest offers by default, and limit (up to 100) and offset page through more.
The command below needs jq, asks only for NM, the condition of SKU 14602, and repeats every five seconds until it prints your listing:
until curl --silent \
'https://api-sandbox.snapcaster.ca/api/v1/marketplace/products/86?condition=NM&limit=100' \
| jq --exit-status --arg code "$SELLER_CODE" \
'.data.offers[] | select(.sellerCode == $code) | {skuId, price, available}'; do
sleep 5
doneRequest
curl --request GET \
--url 'https://api-sandbox.snapcaster.ca/api/v1/marketplace/products/86'{
"success": true,
"data": {
"offers": [
{
"lane": "native",
"productId": 86,
"sellerId": 42,
"storeSlug": null,
"variantId": null,
"skuId": 14602,
"sellerName": "Sandbox Cards",
"condition": "NM",
"language": "EN",
"finish": "Normal",
"isSealed": false,
"price": 24.99,
"available": 3,
"checkoutMode": "express",
"salesCount": 18,
"sellerCode": "sandbox-cards",
"reviewScore": {
"score": 4.93,
"count": 15
},
"poBoxDelivery": {
"status": "available"
},
"disclosures": {
"imageSource": "catalog",
"conditionSource": "seller_declared",
"authenticitySource": "seller_attestation",
"snapcasterInspected": false,
"snapcasterGraded": false,
"snapcasterAuthenticated": false,
"gradingStandard": "snapcaster-magic-v1",
"declaredGrade": "Near Mint",
"buyerProtection": "magic-grade-and-authenticity"
},
"fulfillmentObligation": {
"sku": "exact",
"printing": "exact",
"language": "exact",
"finish": "exact",
"condition": "declared-or-better"
}
}
],
"product": {
"productId": 86,
"name": "Abyssal Nightstalker",
"set": "Portal Second Age",
"rarity": "U",
"collectorNumber": null,
"imageUrl": "https://tcgplayer-cdn.tcgplayer.com/product/86_200w.jpg",
"details": [
{
"label": "Rarity",
"value": "U",
"runs": [
{
"type": "text",
"text": "U",
"emphasis": false
}
]
},
{
"label": "P",
"value": "2",
"runs": [
{
"type": "text",
"text": "2",
"emphasis": false
}
]
}
]
}
}
}Expected result
GET /me reports counts.browsable one higher than in step 1, and the product's offers include one with your sellerCode, the price you pushed in dollars, and the available quantity.
Troubleshooting
If counts.browsable did not rise, read readiness.incompletePrerequisites in GET /me, which lists each open requirement by code. The count also stays the same when SKU 14602 was already browsable before step 2. If GET /listings shows available below quantity, earlier test orders still hold units. The sandbox seller closes each browse readiness requirement with the requests in Finish sandbox seller setup through the account API, and Release the hold on sold units lists what frees held units.
Step 4: receive a test order
After the offer is browsable, trigger the test purchase with POST /sandbox/test-purchases.
Snapcaster’s sandbox buyer buys the lines you name from your own seller and pays in Stripe test mode, on every subscription.
curl --request POST \
--url 'https://api-sandbox.snapcaster.ca/api/v1/marketplace/sandbox/test-purchases' \
--header "Authorization: Bearer $MARKETPLACE_API_KEY" \
--header "Idempotency-Key: $(uuidgen)" \
--header 'Content-Type: application/json' \
--data '{
"lines": [
{
"skuId": 14602,
"quantity": 1
}
]
}'$(uuidgen) makes a new Idempotency-Key each time you run the command. To retry a command after a lost response, send the key from the first attempt.
The trigger returns HTTP 202 with status: processing and a test_purchase_ref.
Set the test_purchase_ref from your response in your shell, as in export TEST_PURCHASE_REF=tpr_3f2c0b9d8e7a6f5c4b3a29180f7e6d5c with your own value.
Then poll GET /sandbox/test-purchases/{ref} with the request in this step until status is succeeded, and set the order id from order_ids in your shell as ORDER_ID for steps 5 and 6.
Request
curl --request GET \
--url "https://api-sandbox.snapcaster.ca/api/v1/marketplace/sandbox/test-purchases/$TEST_PURCHASE_REF" \
--header "Authorization: Bearer $MARKETPLACE_API_KEY"{
"success": true,
"data": {
"test_purchase_ref": "tpr_3f2c0b9d8e7a6f5c4b3a29180f7e6d5c",
"checkout_id": "chk_4e1f7a2b9c3d",
"status": "succeeded",
"order_ids": [
"ord_sandbox_01K4A8Y6D4QJH2N3P5R7S9T0VW"
],
"failure": null
}
}Expected result
The poll returns succeeded, and order_ids names the test order. Within a few seconds your receiver gets the signed order.created, or inventory.adjusted on an inventory subscription.
Troubleshooting
If the poll returns failed, its failure field gives the reason. If it returns processing, the payment is not captured yet, so poll again. TEST_LISTING_NOT_BROWSABLE means step 3 has not passed yet. RATE_LIMITED means your platform started 20 test purchases in the last hour, so wait Retry-After seconds. If the trigger returns TEST_PURCHASE_NOT_AVAILABLE, email support to place the test purchase.
Step 5: report fulfillment
Mark the test order shipped with tracking, using the order id from ORDER_ID.
Request
curl --request PATCH \
--url "https://api-sandbox.snapcaster.ca/api/v1/marketplace/orders/$ORDER_ID/fulfillment" \
--header "Authorization: Bearer $MARKETPLACE_API_KEY" \
--header "Idempotency-Key: $(uuidgen)" \
--header 'Content-Type: application/json' \
--data '{
"status": "shipped",
"trackingCompany": "Canada Post",
"trackingNumber": "CX123456789CA",
"trackingUrl": "https://www.canadapost-postescanada.ca/track-reperage/en#/details/CX123456789CA"
}'$(uuidgen) makes a new Idempotency-Key each time you run the command. To retry a command after a lost response, send the key from the first attempt.
{
"success": true,
"data": {
"order_id": "ord_sandbox_01K4A8Y6D4QJH2N3P5R7S9T0VW",
"seller_id": 42,
"created_at": "2026-09-04T18:42:17.000Z",
"currency": "CAD",
"customer": {
"name": "Alex Morgan",
"email": "alex.morgan@example.test",
"shipping_address": {
"line1": "123 Sandbox Avenue",
"line2": "Unit 4",
"city": "Toronto",
"province": "ON",
"postal_code": "M5V 2T6",
"country": "CA"
}
},
"ship_to": {
"name": "Alex Morgan",
"shipping_address": {
"line1": "123 Sandbox Avenue",
"line2": "Unit 4",
"city": "Toronto",
"province": "ON",
"postal_code": "M5V 2T6",
"country": "CA"
}
},
"address_verification": {
"status": "pending",
"suggested_address": null
},
"lines": [
{
"line_ref": "oln_1f0c8a4d7e2b45c9a1d6f3b8e07c9241",
"sku_id": 14602,
"product_id": 86,
"quantity": 1,
"unit_price": 2499,
"name": "Abyssal Nightstalker",
"set": "Portal Second Age",
"condition": "NM",
"external_ref": "platform-listing-1042",
"collector_number": null,
"language": "English",
"finish": "Non-Foil",
"cancelled_quantity": 0,
"refunded_quantity": 0,
"substitution_credit_cents": 0,
"substitution": null,
"reported_line_ref": null
}
],
"totals": {
"subtotal": 2499,
"shipping": 599,
"tax": 403,
"grand_total": 3501
},
"tax": {
"lines": [
{
"label": "HST",
"rate": 0.13,
"amount": 403
}
]
},
"shipping": {
"method": "Tracked parcel",
"cost": 599,
"is_tracked": true
},
"buyer_notes": "Please protect the card with a top loader.",
"status": "shipped",
"updated_at": "2026-09-04T18:42:17.000Z",
"dispatch": {
"deadline_at": "2026-09-08T23:59:59.000Z",
"dispatched_at": "2026-09-05T12:00:00.000Z",
"overdue_at": null,
"payout_paused": false
},
"cancellations": [],
"refunds": [],
"substitutions": [],
"pending_cancellation_requests": [],
"fulfillment": {
"tracking_company": "Canada Post",
"tracking_number": "CX123456789CA",
"tracking_url": "https://www.canadapost-postescanada.ca/track-reperage/en#/details/CX123456789CA"
}
}
}Expected result
The response is the order with status shipped, the tracking you sent in fulfillment, and dispatch.dispatched_at set.
Troubleshooting
For 422 INVALID_TRANSITION, read the current order status, because an earlier request may have already marked it shipped. For 404 ORDER_NOT_FOUND, check ORDER_ID and the seller's marketplace API key.
Step 6: observe the payout-ready order
Read the order again while you wait for the sandbox payout process, which runs every five minutes. The expected transfer is the grand total less the commission plus processing fee, the commission tax, and any sales tax Snapcaster collected. Fees and payouts explains each deduction.
Request
curl --request GET \
--url "https://api-sandbox.snapcaster.ca/api/v1/marketplace/orders/$ORDER_ID" \
--header "Authorization: Bearer $MARKETPLACE_API_KEY"Expected result
The response is the order step 5 returned, still shipped, with its original totals. The sandbox has no hold window, so its transfer process sends the eligible test amount to the seller's Stripe test-mode connected account.
Troubleshooting
If no transfer appears after about ten minutes, check Stripe test mode and confirm the order is shipped with no cancellation or refund. Ask support to verify the transfer id and amount.
Test cancellations and refunds
Section titled “Test cancellations and refunds”To try a cancellation before shipping and a refund after shipping, trigger a separate test purchase for each test order. Act as the sandbox buyer lists the buyer requests you can raise on them. The sandbox keeps your data between runs, so set the original listing quantity to zero when you finish.
Need help?
The email includes this page and selects sandbox as the environment. Update it if you need help with production.
Never include an API key or signing secret in the message.