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:

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:

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.