# Sandbox quickstart

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

You need these from [Credentials and onboarding](https://developers.snapcaster.ca/guide/credentials-and-sandbox.md) and the 1Password share Snapcaster sends you:

- The sandbox seller’s marketplace API key, set in your shell as `MARKETPLACE_API_KEY`, such as `export 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](https://developers.snapcaster.ca/guide/webhook-receiver.md) gives you one to run, and [Test your receiver](https://developers.snapcaster.ca/guide/webhook-signing.md#test-your-receiver) checks it.
- `skuId` 14602 and `productId` 86 from the [sandbox catalog SKUs](https://developers.snapcaster.ca/guide/sandbox.md#sandbox-guarantees), 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](https://developers.snapcaster.ca/guide/sandbox.md#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

```shell title="Step 1 cURL request - Sandbox"
curl --request GET \
  --url 'https://api-sandbox.snapcaster.ca/api/v1/marketplace/me' \
  --header "Authorization: Bearer $MARKETPLACE_API_KEY"
```

```json title="Step 1 curated response"
{
  "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.

[View the full Get store status reference](https://developers.snapcaster.ca/reference/store/get-store.md)

## 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](https://developers.snapcaster.ca/guide/sync-a-catalog.md).

### Request

```shell title="Step 2 cURL request - Sandbox"
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`.

```json title="Step 2 curated response"
{
  "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.

[View the full Upsert listings reference](https://developers.snapcaster.ca/reference/listings/upsert-listings.md)

## 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:

```shell title="Poll the product until your listing appears"
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
done
```

### Request

```shell title="Step 3 cURL request - Sandbox"
curl --request GET \
  --url 'https://api-sandbox.snapcaster.ca/api/v1/marketplace/products/86'
```

```json title="Step 3 curated response"
{
  "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](https://developers.snapcaster.ca/guide/core-concepts.md#browse-readiness) requirement with the requests in [Finish sandbox seller setup through the account API](https://developers.snapcaster.ca/guide/sandbox.md#finish-sandbox-seller-setup-through-the-account-api), and [Release the hold on sold units](https://developers.snapcaster.ca/guide/webhook-receiver.md#release-the-hold-on-sold-units) lists what frees held units.

[View the full Get product offers reference](https://developers.snapcaster.ca/reference/browse/get-product.md)

## Step 4: receive a test order

After the offer is browsable, trigger the test purchase with [`POST /sandbox/test-purchases`](https://developers.snapcaster.ca/reference/sandbox/create-sandbox-test-purchase.md). Snapcaster’s sandbox buyer buys the lines you name from your own seller and pays in Stripe test mode, on every subscription.

```shell title="Trigger the test purchase"
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}`](https://developers.snapcaster.ca/reference/sandbox/get-sandbox-test-purchase.md) 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

```shell title="Step 4 cURL request - Sandbox"
curl --request GET \
  --url "https://api-sandbox.snapcaster.ca/api/v1/marketplace/sandbox/test-purchases/$TEST_PURCHASE_REF" \
  --header "Authorization: Bearer $MARKETPLACE_API_KEY"
```

```json title="Step 4 curated response"
{
  "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.

[View the full Read a sandbox test purchase reference](https://developers.snapcaster.ca/reference/sandbox/get-sandbox-test-purchase.md)

## Step 5: report fulfillment

Mark the test order shipped with tracking, using the order id from `ORDER_ID`.

### Request

```shell title="Step 5 cURL request - Sandbox"
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.

```json title="Step 5 curated response"
{
  "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.

[View the full Update fulfillment reference](https://developers.snapcaster.ca/reference/orders/report-fulfillment.md)

## 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](https://developers.snapcaster.ca/guide/go-live.md#fees-and-payouts) explains each deduction.

### Request

```shell title="Step 6 cURL request - Sandbox"
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.

[View the full Get one order reference](https://developers.snapcaster.ca/reference/orders/get-order.md)

## 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](https://developers.snapcaster.ca/guide/sandbox.md#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?

[Email Marketplace support.](mailto:info@snapcaster.gg?subject=Marketplace%20docs%20help%3A%20%2Fguide%2Fgolden-path\&body=page%3A%20%2Fguide%2Fgolden-path%0Aenvironment%3A%20sandbox%0A%0ADescribe%20what%20you%20observed%3A%20)

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.
