API reference
Base URL: https://dev.ddex-delivery.phyzi.io/v1. Authenticate every request with Authorization: Bearer pk_…. Request and
response bodies are JSON. The machine-readable version is the OpenAPI spec.
| Method | Path | Idempotency-Key | |
|---|---|---|---|
| GET | /v1/me |
Which partner and environment your key belongs to | |
| POST | /v1/validate |
Check an ERN XML (body = the XML) | |
| GET | /v1/deliveries · /v1/deliveries/{batchId} |
Delivery results | |
| GET | /v1/releases · /v1/releases/{releaseId} |
Validated releases | |
| GET | /v1/releases/{releaseId}/availability |
Can fans buy it right now? | |
| POST | /v1/master-orders |
required | Order a master + units for an artist |
| GET | /v1/master-orders · /v1/master-orders/{id} |
?release_id= filter on the list |
|
| POST | /v1/proofs/{id}/approve · /v1/proofs/{id}/reject |
required | Artist's proof decision |
| POST | /v1/fan-orders |
required | A fan bought a drive |
| GET | /v1/fan-orders · /v1/fan-orders/{id} |
?release_id= filter on the list |
|
| POST | /v1/fan-orders/{id}/cancel |
required | Cancel before it ships |
| GET, POST | /v1/webhook-endpoints |
List / register endpoints | |
| PATCH, DELETE | /v1/webhook-endpoints/{id} |
Change URL, enable/disable, remove | |
| POST | /v1/webhook-endpoints/{id}/roll-secret |
required | Rotate the signing secret |
| POST | /v1/webhook-endpoints/{id}/test |
Send a ping |
|
| GET | /v1/events · /v1/events/{id} |
Event log (?type=), with delivery attempts on the single event |
|
| POST | /v1/events/{id}/resend |
Re-send an event |
Sandbox-only endpoints are listed in Sandbox & testing.
Releases
A release exists once a delivery for it validates. Its id is your release ID.
{
"id": "aria-rel-sample-0001",
"object": "release",
"status": "active",
"title": "Sample Album",
"artist": "Phyzi Sample Artist",
"release_type": "Album",
"genre": "Hip Hop",
"upc": null,
"track_count": 3,
"tracks": [{ "disc": 1, "number": 1, "title": "Opening Light", "artist": "Phyzi Sample Artist", "isrc": null, "duration_seconds": 3 }],
"videos": [{ "disc": 1, "number": 4, "title": "Night Drive (Official Video)", "artist": "Phyzi Sample Artist feat. Guest Singer", "duration_seconds": 3, "format": "MP4 H.264", "width": 640, "height": 360 }],
"cover": { "width": 3000, "height": 3000 },
"estimated_drive_bytes": 2133199,
"print_files": [{ "role": "drive_art" }],
"latest_batch_id": "20261101120000000",
"fulfillment_mode": "prepaid_stock",
"stock_on_hand": 0,
"created_at": "2026-11-02T18:04:13.950Z",
"updated_at": "2026-11-02T18:04:13.950Z"
}
status is active or taken_down.
Availability
GET /v1/releases/{releaseId}/availability. Use it to decide whether to show "Buy Phyzi".
{ "object": "availability", "release_id": "aria-rel-sample-0001", "available": true, "fulfillment_mode": "prepaid_stock", "stock_on_hand": 50, "max_quantity": 10 }
Phyzi sets each release's fulfillment mode:
prepaid_stock(default): fans buy from units the artist already paid for (units_to_stockon a master order).availableisfalsewhen stock runs out, until the artist reorders.made_to_order: always available. Fan orders arequeuedand produced in batches.stock_on_handisnull.
Master orders
An artist orders a Phyzi master plus units. Units are split between fan stock held at Phyzi
(units_to_stock) and units shipped to the artist (units_to_artist); either can be 0.
POST /v1/master-orders
| Field | Type | |
|---|---|---|
release_id |
string | required. A validated, active release. |
external_id |
string | optional. Your order ID, unique per partner. |
artist |
{ name, email } |
required. The artist is Phyzi's customer of record. |
units_to_stock |
integer ≥ 0 | required |
units_to_artist |
integer ≥ 0 | required. The total must be ≥ 1. |
packaging |
poly_bag | custom_box |
default poly_bag. custom_box uses the box_art print file. |
ship_to |
Address | required when units_to_artist > 0 |
notes |
string | optional, max 2000 characters |
Returns 201 with the master order:
{
"id": "mo_8f3kq2w7m1x9v4t6b0zr",
"object": "master_order",
"external_id": "aria-order-123",
"release_id": "aria-rel-sample-0001",
"status": "created",
"units_to_stock": 50,
"units_to_artist": 10,
"packaging": "poly_bag",
"artist": { "name": "Phyzi Sample Artist", "email": "artist@example.com" },
"ship_to": { "name": "Phyzi Sample Artist", "line1": "1 Main St", "city": "Oklahoma City", "region": "OK", "postal_code": "73102", "country": "US" },
"notes": null,
"timeline": { "created_at": "2026-11-02T18:10:00.000Z" },
"proofs": [],
"shipment": null,
"created_at": "2026-11-02T18:10:00.000Z",
"updated_at": "2026-11-02T18:10:00.000Z"
}
Statuses: created → proof_ready → proof_approved → master_built → units_loaded →
shipped (or cancelled). A rejected proof sends the order back to created until the next proof round.
timeline gets a <step>_at timestamp for each step. When it ships, shipment holds the artist's
tracking, and units_to_stock are added to the release's fan stock (stock.updated).
Errors: 404 resource_missing (unknown release), 409 release_taken_down, 409 duplicate_external_id.
Proofs
Before anything is printed, Phyzi sends a proof of the print art (master_order.proof_ready). Each
entry in proofs looks like this:
{ "id": "pf_…", "object": "proof", "round": 1, "status": "pending", "images": [{ "role": "drive_art", "url": "https://…" }], "note": null, "created_at": "…", "decided_at": null }
Image urls are signed links valid for 7 days, and a fresh one comes back with every GET. Show
them to the artist, then send their decision:
POST /v1/proofs/{id}/approvewith optional{ "note": "…" }POST /v1/proofs/{id}/rejectwith required{ "note": "what should change" }
Fan orders
POST /v1/fan-orders
| Field | Type | |
|---|---|---|
release_id |
string | required |
external_id |
string | required. Your order ID, unique per partner. |
quantity |
integer 1–10 | default 1 |
customer |
{ name, email? } |
required |
ship_to |
Address | required |
No payment data. You've already charged the fan.
Returns 201 with status accepted (prepaid stock, units reserved) or queued (made to order).
If there isn't enough stock you get 409 out_of_stock with the current availability, and nothing
is reserved:
{ "error": { "type": "conflict", "code": "out_of_stock", "message": "Only 0 units of aria-rel-sample-0001 in stock.", "availability": { "available": false, "stock_on_hand": 0, "...": "..." } } }
POST /v1/fan-orders/{id}/cancel works until the order ships, and any reserved units go back into
stock. Once shipped, shipment holds carrier, service, tracking_number, tracking_url and shipped_at.
Addresses
{ "name": "Jane Fan", "company": null, "line1": "500 Oak Ave", "line2": "Apt 4", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "phone": "+15125550100", "email": "jane@example.com" }
name, line1, city, postal_code and country (ISO 3166-1 alpha-2) are required.
Webhook endpoints
POST /v1/webhook-endpoints with { "url": "https://…" } returns 201 and the endpoint, including
its signing secret. This is the only time the secret is shown (apart from after a roll-secret).
PATCH takes url and/or enabled. You can register several endpoints, and each gets every event.
See Webhooks.
Events
GET /v1/events lists every event we've sent you, newest first (filter with ?type=fan_order.shipped).
GET /v1/events/{id} adds delivery_attempts (HTTP status, error, timing), which is the quickest
way to debug an endpoint. POST /v1/events/{id}/resend queues it again.