openapi: 3.1.0
info:
  title: Phyzi Partner API
  version: "1.0"
  description: |
    Deliver releases (DDEX ERN 4.3 via S3), order Phyzi masters for artists and drives for fans,
    and receive signed webhooks. Docs: https://dev.integrate.phyzi.io/docs
servers:
  - url: https://dev.ddex-delivery.phyzi.io
    description: Sandbox (pk_test_ keys)
  - url: https://prod.ddex-delivery.phyzi.io
    description: Live (pk_live_ keys)
security:
  - bearer: []
components:
  securitySchemes:
    bearer: { type: http, scheme: bearer, description: "pk_test_… or pk_live_…" }
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Unique per logical request. Same key + same body replays the first response.
      schema: { type: string, maxLength: 255 }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
    ReleaseFilter:
      name: release_id
      in: query
      schema: { type: string }
  responses:
    Error:
      description: Error
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [type, code, message]
          properties:
            type: { type: string, enum: [authentication_error, invalid_request_error, not_found, conflict, idempotency_error, api_error] }
            code: { type: string, examples: [out_of_stock, resource_missing, parameter_invalid] }
            message: { type: string }
            param: { type: string }
          additionalProperties: true
    Issue:
      type: object
      required: [code, message]
      properties:
        code: { type: string, examples: [image.too_small] }
        message: { type: string }
        path: { type: string }
    Delivery:
      type: object
      properties:
        id: { type: string, description: Batch ID (folder name) }
        object: { const: delivery }
        status: { type: string, enum: [received, validated, rejected] }
        message_type: { type: [string, "null"], enum: [NewReleaseMessage, PurgeReleaseMessage, null] }
        release_id: { type: [string, "null"] }
        errors: { type: array, items: { $ref: "#/components/schemas/Issue" } }
        warnings: { type: array, items: { $ref: "#/components/schemas/Issue" } }
        received_at: { type: string, format: date-time }
        completed_at: { type: [string, "null"], format: date-time }
    Release:
      type: object
      properties:
        id: { type: string, description: Your release ID (ERN ProprietaryId) }
        object: { const: release }
        status: { type: string, enum: [active, taken_down] }
        title: { type: string }
        artist: { type: string }
        release_type: { type: string, enum: [Album, EP, Single] }
        genre: { type: string }
        upc: { type: [string, "null"] }
        track_count: { type: integer }
        tracks:
          type: array
          items:
            type: object
            properties:
              disc: { type: integer }
              number: { type: integer }
              title: { type: string }
              artist: { type: string }
              isrc: { type: [string, "null"] }
              duration_seconds: { type: [number, "null"] }
        videos:
          type: array
          items:
            type: object
            properties:
              disc: { type: integer }
              number: { type: integer }
              title: { type: string }
              artist: { type: string }
              duration_seconds: { type: [number, "null"] }
              format: { type: [string, "null"], examples: ["MP4 H.264"] }
              width: { type: [integer, "null"] }
              height: { type: [integer, "null"] }
        cover: { type: [object, "null"], properties: { width: { type: integer }, height: { type: integer } } }
        estimated_drive_bytes: { type: [integer, "null"], description: "Space the release takes on a Phyzi (limit about 7.36 GB)" }
        print_files: { type: array, items: { type: object, properties: { role: { type: string, enum: [drive_art, box_art] } } } }
        latest_batch_id: { type: string }
        fulfillment_mode: { type: string, enum: [prepaid_stock, made_to_order] }
        stock_on_hand: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Availability:
      type: object
      properties:
        object: { const: availability }
        release_id: { type: string }
        available: { type: boolean }
        fulfillment_mode: { type: string, enum: [prepaid_stock, made_to_order] }
        stock_on_hand: { type: [integer, "null"] }
        max_quantity: { type: integer }
    Address:
      type: object
      required: [name, line1, city, postal_code, country]
      properties:
        name: { type: string, maxLength: 200 }
        company: { type: string, maxLength: 200 }
        line1: { type: string, maxLength: 200 }
        line2: { type: string, maxLength: 200 }
        city: { type: string, maxLength: 100 }
        region: { type: string, maxLength: 100 }
        postal_code: { type: string, maxLength: 20 }
        country: { type: string, pattern: "^[A-Z]{2}$" }
        phone: { type: string, maxLength: 40 }
        email: { type: string, format: email }
    Proof:
      type: object
      properties:
        id: { type: string }
        object: { const: proof }
        round: { type: integer }
        status: { type: string, enum: [pending, approved, rejected] }
        images: { type: array, items: { type: object, properties: { role: { type: string }, url: { type: [string, "null"], format: uri, description: "Signed link, valid 7 days" } } } }
        note: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        decided_at: { type: [string, "null"], format: date-time }
    Shipment:
      type: [object, "null"]
      properties:
        destination: { type: string }
        units: { type: integer }
        carrier: { type: string }
        service: { type: string }
        tracking_number: { type: string }
        tracking_url: { type: string, format: uri }
        shipped_at: { type: string, format: date-time }
    MasterOrderCreate:
      type: object
      required: [release_id, artist, units_to_stock, units_to_artist]
      properties:
        release_id: { type: string }
        external_id: { type: string, maxLength: 100 }
        artist: { type: object, required: [name, email], properties: { name: { type: string }, email: { type: string, format: email } } }
        units_to_stock: { type: integer, minimum: 0 }
        units_to_artist: { type: integer, minimum: 0 }
        packaging: { type: string, enum: [poly_bag, custom_box], default: poly_bag }
        ship_to: { $ref: "#/components/schemas/Address" }
        notes: { type: string, maxLength: 2000 }
    MasterOrder:
      type: object
      properties:
        id: { type: string }
        object: { const: master_order }
        external_id: { type: [string, "null"] }
        release_id: { type: string }
        status: { type: string, enum: [created, proof_ready, proof_approved, master_built, units_loaded, shipped, cancelled] }
        units_to_stock: { type: integer }
        units_to_artist: { type: integer }
        packaging: { type: string }
        artist: { type: object }
        ship_to: { oneOf: [{ $ref: "#/components/schemas/Address" }, { type: "null" }] }
        notes: { type: [string, "null"] }
        timeline: { type: object, additionalProperties: { type: string, format: date-time } }
        proofs: { type: array, items: { $ref: "#/components/schemas/Proof" } }
        shipment: { $ref: "#/components/schemas/Shipment" }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    FanOrderCreate:
      type: object
      required: [release_id, external_id, customer, ship_to]
      properties:
        release_id: { type: string }
        external_id: { type: string, maxLength: 100 }
        quantity: { type: integer, minimum: 1, maximum: 10, default: 1 }
        customer: { type: object, required: [name], properties: { name: { type: string }, email: { type: string, format: email } } }
        ship_to: { $ref: "#/components/schemas/Address" }
    FanOrder:
      type: object
      properties:
        id: { type: string }
        object: { const: fan_order }
        external_id: { type: string }
        release_id: { type: string }
        status: { type: string, enum: [accepted, queued, shipped, cancelled] }
        quantity: { type: integer }
        customer: { type: object }
        ship_to: { $ref: "#/components/schemas/Address" }
        shipment: { $ref: "#/components/schemas/Shipment" }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    WebhookEndpoint:
      type: object
      properties:
        id: { type: string }
        object: { const: webhook_endpoint }
        url: { type: string, format: uri }
        enabled: { type: boolean }
        secret: { type: string, description: Only on create and roll-secret }
        active_secrets: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Event:
      type: object
      properties:
        id: { type: string }
        type:
          type: string
          enum: [ping, release.received, release.validated, release.rejected, release.taken_down, master_order.created, master_order.proof_ready, master_order.proof_approved, master_order.proof_rejected, master_order.master_built, master_order.units_loaded, master_order.shipped, master_order.cancelled, fan_order.accepted, fan_order.shipped, fan_order.cancelled, stock.updated]
        created_at: { type: string, format: date-time }
        livemode: { type: boolean }
        data: { type: object }
    List:
      type: object
      properties:
        object: { const: list }
        data: { type: array, items: {} }
        has_more: { type: boolean }
paths:
  /v1/me:
    get:
      summary: Who am I
      responses: { "200": { description: Partner, content: { application/json: { schema: { type: object } } } }, "401": { $ref: "#/components/responses/Error" } }
  /v1/validate:
    post:
      summary: Validate an ERN XML (no files)
      requestBody: { required: true, content: { application/xml: { schema: { type: string } } } }
      responses: { "200": { description: Validation result, content: { application/json: { schema: { type: object } } } } }
  /v1/deliveries:
    get:
      summary: List deliveries
      parameters: [{ $ref: "#/components/parameters/Limit" }]
      responses: { "200": { description: List of Delivery, content: { application/json: { schema: { $ref: "#/components/schemas/List" } } } } }
  /v1/deliveries/{batchId}:
    get:
      summary: Get a delivery
      parameters: [{ name: batchId, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: Delivery, content: { application/json: { schema: { $ref: "#/components/schemas/Delivery" } } } }, "404": { $ref: "#/components/responses/Error" } }
  /v1/releases:
    get:
      summary: List releases
      parameters: [{ $ref: "#/components/parameters/Limit" }]
      responses: { "200": { description: List of Release, content: { application/json: { schema: { $ref: "#/components/schemas/List" } } } } }
  /v1/releases/{releaseId}:
    get:
      summary: Get a release
      parameters: [{ name: releaseId, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: Release, content: { application/json: { schema: { $ref: "#/components/schemas/Release" } } } }, "404": { $ref: "#/components/responses/Error" } }
  /v1/releases/{releaseId}/availability:
    get:
      summary: Fan availability
      parameters: [{ name: releaseId, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: Availability, content: { application/json: { schema: { $ref: "#/components/schemas/Availability" } } } } }
  /v1/master-orders:
    get:
      summary: List master orders
      parameters: [{ $ref: "#/components/parameters/Limit" }, { $ref: "#/components/parameters/ReleaseFilter" }]
      responses: { "200": { description: List of MasterOrder, content: { application/json: { schema: { $ref: "#/components/schemas/List" } } } } }
    post:
      summary: Create a master order
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/MasterOrderCreate" } } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/MasterOrder" } } } }
        "400": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
  /v1/master-orders/{id}:
    get:
      summary: Get a master order
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: MasterOrder, content: { application/json: { schema: { $ref: "#/components/schemas/MasterOrder" } } } } }
  /v1/proofs/{id}/approve:
    post:
      summary: Approve a proof
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }, { $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { content: { application/json: { schema: { type: object, properties: { note: { type: string } } } } } }
      responses: { "200": { description: MasterOrder, content: { application/json: { schema: { $ref: "#/components/schemas/MasterOrder" } } } } }
  /v1/proofs/{id}/reject:
    post:
      summary: Reject a proof
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }, { $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [note], properties: { note: { type: string } } } } } }
      responses: { "200": { description: MasterOrder, content: { application/json: { schema: { $ref: "#/components/schemas/MasterOrder" } } } } }
  /v1/fan-orders:
    get:
      summary: List fan orders
      parameters: [{ $ref: "#/components/parameters/Limit" }, { $ref: "#/components/parameters/ReleaseFilter" }]
      responses: { "200": { description: List of FanOrder, content: { application/json: { schema: { $ref: "#/components/schemas/List" } } } } }
    post:
      summary: Create a fan order
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/FanOrderCreate" } } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/FanOrder" } } } }
        "409": { description: "out_of_stock, duplicate_external_id or release_taken_down", content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
  /v1/fan-orders/{id}:
    get:
      summary: Get a fan order
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: FanOrder, content: { application/json: { schema: { $ref: "#/components/schemas/FanOrder" } } } } }
  /v1/fan-orders/{id}/cancel:
    post:
      summary: Cancel a fan order (before it ships)
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }, { $ref: "#/components/parameters/IdempotencyKey" }]
      responses: { "200": { description: FanOrder, content: { application/json: { schema: { $ref: "#/components/schemas/FanOrder" } } } } }
  /v1/webhook-endpoints:
    get:
      summary: List webhook endpoints
      responses: { "200": { description: List of WebhookEndpoint, content: { application/json: { schema: { $ref: "#/components/schemas/List" } } } } }
    post:
      summary: Register a webhook endpoint
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [url], properties: { url: { type: string, format: uri } } } } } }
      responses: { "201": { description: WebhookEndpoint with secret, content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } } } }
  /v1/webhook-endpoints/{id}:
    patch:
      summary: Update an endpoint
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      requestBody: { content: { application/json: { schema: { type: object, properties: { url: { type: string, format: uri }, enabled: { type: boolean } } } } } }
      responses: { "200": { description: WebhookEndpoint, content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } } } }
    delete:
      summary: Remove an endpoint
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: Deleted } }
  /v1/webhook-endpoints/{id}/roll-secret:
    post:
      summary: Rotate the signing secret
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }, { $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { content: { application/json: { schema: { type: object, properties: { expire_previous: { type: boolean, default: false } } } } } }
      responses: { "200": { description: WebhookEndpoint, content: { application/json: { schema: { $ref: "#/components/schemas/WebhookEndpoint" } } } } }
  /v1/webhook-endpoints/{id}/test:
    post:
      summary: Send a ping event
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { "202": { description: Event, content: { application/json: { schema: { $ref: "#/components/schemas/Event" } } } } }
  /v1/events:
    get:
      summary: List events
      parameters: [{ $ref: "#/components/parameters/Limit" }, { name: type, in: query, schema: { type: string } }]
      responses: { "200": { description: List of Event, content: { application/json: { schema: { $ref: "#/components/schemas/List" } } } } }
  /v1/events/{id}:
    get:
      summary: Get an event with delivery attempts
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: Event, content: { application/json: { schema: { $ref: "#/components/schemas/Event" } } } } }
  /v1/events/{id}/resend:
    post:
      summary: Re-send an event
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { "202": { description: Queued } }
  /v1/sandbox/deliveries/sample:
    post:
      summary: "Sandbox: deliver the sample package"
      responses: { "202": { description: Delivery started } }
  /v1/sandbox/master-orders/{id}/advance:
    post:
      summary: "Sandbox: advance a master order one step"
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: MasterOrder, content: { application/json: { schema: { $ref: "#/components/schemas/MasterOrder" } } } }, "409": { $ref: "#/components/responses/Error" } }
  /v1/sandbox/fan-orders/{id}/ship:
    post:
      summary: "Sandbox: ship a fan order"
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: FanOrder, content: { application/json: { schema: { $ref: "#/components/schemas/FanOrder" } } } } }
  /v1/sandbox/releases/{releaseId}/stock:
    post:
      summary: "Sandbox: change fulfilment mode or stock"
      parameters: [{ name: releaseId, in: path, required: true, schema: { type: string } }]
      requestBody: { content: { application/json: { schema: { type: object, properties: { fulfillment_mode: { type: string, enum: [prepaid_stock, made_to_order] }, add_units: { type: integer } } } } } }
      responses: { "200": { description: Availability, content: { application/json: { schema: { $ref: "#/components/schemas/Availability" } } } } }
