> ## Documentation Index
> Fetch the complete documentation index at: https://dev.phygitals.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox

> Test the Phygitals Partner API end-to-end without consuming inventory, moving real money, or shipping real cards.

The sandbox is a read-only mirror of the production Partner API. Use it to integrate, run end-to-end tests, and demo the full purchase → inventory → sellback → shipping flow without consuming real inventory, settling real money, or triggering on-chain transactions.

## Base URLs

| Environment    | Base URL                      |
| -------------- | ----------------------------- |
| **Production** | `https://api.phygitals.com`   |
| **Sandbox**    | `https://api.phygitals.com/_` |

All endpoints are versioned under `/api/v1/`. The sandbox mirrors every production route under `/_/api/v1/`. For example:

| Production                 | Sandbox                      |
| -------------------------- | ---------------------------- |
| `GET /api/v1/vm/available` | `GET /_/api/v1/vm/available` |
| `POST /api/v1/vm/buy/init` | `POST /_/api/v1/vm/buy/init` |
| `POST /api/v1/vm/buyback`  | `POST /_/api/v1/vm/buyback`  |

## API keys

Two key tiers are issued by Phygitals:

<CardGroup cols={2}>
  <Card title="Production keys" icon="key">
    Identify your partner account and authorize live purchases, sellbacks, and shipments. Contact [hello@phygitals.com](mailto:hello@phygitals.com) to request one.
  </Card>

  <Card title="Sandbox keys" icon="flask">
    Prefixed with `sandbox-`. Authorize the sandbox environment at `/_/api/v1/*`. Do **not** identify a partner. Never produce real money movement, on-chain transactions, or shipments. Use them to integrate without consuming inventory.
  </Card>
</CardGroup>

A `401 { "error": "Invalid API key" }` is returned for missing, empty, or unrecognized `X-API-Key` headers on any endpoint, in any environment.

## How sandbox differs from production

Read this before integrating. Every behavior below is sandbox-only and must not be relied on in production code paths.

### State is in-memory and ephemeral

Sandbox state (purchase sessions, simulated inventory, sellbacks, shipping orders) lives in the API server's memory and is **wiped on restart or deploy**. Multi-instance deployments do **not** share state. Treat every sandbox session as fresh.

### No real fulfillment

The sandbox never touches Alt, PSA, or Fanatics. There are no on-chain transactions, no payment processing, no carrier API calls, and no actual shipping label creation. Every fulfillment is simulated.

### No row locking on purchases

The same underlying eBay listing can be returned to multiple users from concurrent `buy/init` calls. The sandbox does not reserve inventory. Don't use sandbox to test "two users racing for the same item" scenarios. That's a production-only behavior.

### Synchronous fulfillment

`POST /vm/buy/init` returns the pulled items immediately. As a result:

* `POST /vm/buy/status` **never** returns the `{ "status": "pending" }` envelope in sandbox. It returns either the fulfilled `{ result: ... }` payload or `400 { "error": "Transaction failed" }`.
* Production keeps the polling envelope as-is, so build your client to handle pending. Sandbox just won't exercise that branch.

### No idempotency

Calling `POST /vm/buy/init` twice with identical bodies produces two distinct `session_id`s and stacks the items in inventory. Don't rely on idempotency keys in sandbox.

### No rate limiting

The sandbox never returns `429`. Rate limits exist in production but are not enforced here.

### Frozen pricing on sellback

The `amount` returned from `POST /vm/buyback` equals the `buyback_price` assigned at `buy/init` time. Prices are frozen and there is no Alt oracle re-fetch. Production re-prices live at sellback time.

### No vouchers

Voucher / promo logic on sellbacks is production-only.

### Address validation is real; country allow-list is stubbed

* `destination` is validated end-to-end through the Google Address Validation API. **`GOOGLE_MAPS_API_KEY` must be configured server-side** for `/ship/quote` to succeed. When the key is missing, or when Google returns a non-2xx, every quote 400s with `{ "error": "Invalid destination address", "details": "Validation failed: internal error" }` — the validator fails closed rather than letting bad addresses through. Successful validation returns `{ "error": "Invalid destination address", "details": ..., "suggested": ... }` for invalid input.
* `country` is also checked separately for length (must be 2 chars). Any 2-character string is accepted, including `"ZZ"`. Production validates against an actual carrier-supported allow-list.

### Shipping orders never advance

A successful `ship/request` in sandbox creates an order with `status: "queued"` and stays queued. `tracking_number`, `tracking_url`, `shipped_at`, and `delivered_at` remain `null` for the lifetime of the sandbox process. Production cycles through `queued → processing → label_created → shipped → delivered`.

### Sandbox is scoped by `user_id` only

Partner identity is not derived from the API key in sandbox. All sandbox state is isolated by the `user_id` you submit. In production, scoping is partner-aware.

## Production-only features

Don't integrate against these in sandbox. They're stubbed or absent and behavior will diverge:

* `429` rate-limit responses on `/vm/buyback` (no concurrency control in sandbox).
* The `{ status: "pending" }` polling envelope on `/vm/buy/status`.
* The `{ order_id: null, status: "error", error_message }` envelope on `/ship/request`.
* Webhook callbacks for shipping status transitions or fulfillment events.
* Voucher / promo logic on sellbacks.
* Real Alt pricing-oracle FMV refresh on sellback.
* Real country allow-list for shipping.
* Real inventory reservation. The same listing can be pulled by multiple users.
* Cross-process / cross-deploy state persistence.

## Error reference

Every error string the sandbox can emit, exactly as returned. Match against these verbatim if you're building error-handling logic.

```text theme={"dark"}
Invalid API key
user_id is required
VM not found
Amount must be greater than 0
Amount must be less than or equal to N           (N = pack.max_per_mint)
Claw machine is out of stock
Error during transaction
Transaction failed
User not found
Card not found
item_id is required
Item not found
Item has expired
item_ids is required
Invalid destination address
Country not supported
quote_id is required
Quote expired
Item already shipped
Failed to fetch shipping rates
Failed to create shipping request
Order not found
An unexpected error occurred
```

Production may emit a superset of these strings. Treat any message not in this list as production-only.
