{
  "openapi": "3.1.0",
  "info": {
    "title": "DYJ Marketplace Partner API",
    "version": "1.0.0",
    "summary": "Server-to-server access to the jewellery products your feeds select.",
    "description": "This API answers one question: **what is currently in my feed, and what does it cost me?**\n\nYou do not pick products. You define *rules* in your DYJ dashboard — categories, a cost\nband, and a margin schedule — and the feed is whatever satisfies them right now. A designer\nlisting a new ring inside your band appears here without either of you doing anything; a\ndelisted one disappears the same way. There is nothing to synchronise and nothing that can\ngo stale, because membership is recomputed on every request rather than stored.\n\nTwo consequences worth planning for:\n\n* **Products can disappear between two calls.** That is normal, not an error. Treat a\n  product that has vanished from search as out of stock rather than as a failure.\n* **Prices are recomputed too.** `retailMinor` is derived from the designer's current cost\n  and your current margin schedule. Cache it for minutes, not days.\n\nEvery amount in this API is in **minor units** — cents. `150000` is €1,500.00. There is no\nendpoint that speaks euros, deliberately: mixing the two is the single most common way an\nintegration ends up off by a factor of a hundred.\n",
    "contact": {
      "name": "DYJ.one",
      "url": "https://dyj.one"
    }
  },
  "servers": [
    {
      "url": "https://api.dyj.one/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "SecretKey": []
    }
  ],
  "tags": [
    {
      "name": "Feeds",
      "description": "Reading the products your rules select."
    },
    {
      "name": "Service",
      "description": "Liveness."
    }
  ],
  "paths": {
    "/market/feeds/{feedId}/items/search": {
      "post": {
        "tags": [
          "Feeds"
        ],
        "summary": "List the products currently in a feed",
        "operationId": "searchFeedItems",
        "description": "Returns the products your feed's rules select, each priced by the first rule that\nmatches it, newest cursor last.\n\n**Filters narrow; they never widen.** Every field in the request body is applied *on\ntop of* your rules. There is no parameter that can reach a product your rules do not\nalready select — asking for a category no rule covers returns an empty list, not an\nerror.\n\nA `POST` for a read is deliberate: the filters are structured, and putting them in a\nbody keeps them out of proxy logs that sit next to your `Authorization` header.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/FeedId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SearchRequest"
              },
              "examples": {
                "everything": {
                  "summary": "The whole feed, first page",
                  "value": {}
                },
                "narrowed": {
                  "summary": "Rings only, at least €1,000.00 of cost",
                  "value": {
                    "categories": [
                      "ring"
                    ],
                    "costMinMinor": 100000
                  }
                },
                "nextPage": {
                  "summary": "The page after a previous response",
                  "value": {
                    "cursor": 412,
                    "limit": 100
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The current contents of the feed.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "examples": [
                    "private, max-age=60"
                  ]
                },
                "description": "Membership is live, but need not be fetched more than once a minute."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                },
                "examples": {
                  "oneProduct": {
                    "value": {
                      "items": [
                        {
                          "id": "2b0478f5-8ef3-41e0-9f1c-a0dcc9a2d993",
                          "title": "Colette solitaire",
                          "description": "A narrow-shouldered solitaire.",
                          "category": "ring",
                          "tags": [
                            "solitaire",
                            "bridal"
                          ],
                          "materialsNote": "18k white gold",
                          "costMinor": 150000,
                          "retailMinor": 181500,
                          "currency": "EUR",
                          "matchedRuleId": 2,
                          "updatedAt": "2026-08-31T18:10:43.594543+00:00"
                        }
                      ],
                      "nextCursor": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/FeedNotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/market/feeds/{feedId}/items/{listingId}": {
      "get": {
        "tags": [
          "Feeds"
        ],
        "summary": "Fetch one product from a feed",
        "operationId": "getFeedItem",
        "description": "Returns a single product, priced exactly as search prices it.\n\nA product is reachable by id **only if your rules still select it**. A product that\nexists on the marketplace but is not in your feed answers `404`, identically to one\nthat does not exist at all — so this endpoint cannot be used to discover what else is\non the platform.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/FeedId"
          },
          {
            "name": "listingId",
            "in": "path",
            "required": true,
            "description": "The product's `id` from a search response.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The product.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Product"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "description": "No such product in this feed. Returned for an unknown id, a product outside your\nfeed, and a delisted one alike.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "notFound": {
                    "value": {
                      "error": "not_found",
                      "message": "Product not found"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "Service"
        ],
        "summary": "Liveness",
        "operationId": "health",
        "security": [],
        "description": "Unauthenticated. Reports that the API process is serving; it does not probe the database.",
        "responses": {
          "200": {
            "description": "Serving.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "examples": [
                        "ok"
                      ]
                    },
                    "service": {
                      "type": "string",
                      "examples": [
                        "client-api"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "SecretKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your **secret** partner key, `dyj_sk_` followed by 48 hex characters. Generate and\nrotate it under *My Account → Developer* in the DYJ dashboard.\n\nIt is shown **once**, at creation. DYJ stores only a SHA-256 hash and cannot recover\nit — if you lose it, rotate.\n\nThis key must live on your server and nowhere else. It is not the same credential as\nthe `dyj_live_` embed key, which is designed for browsers and is rejected here. If\nyour storefront is static, put a small proxy in front of this API rather than shipping\nthe key to the client. Rotation invalidates the previous key immediately; there is no\noverlap window.\n"
      }
    },
    "parameters": {
      "FeedId": {
        "name": "feedId",
        "in": "path",
        "required": true,
        "description": "Your feed's public id, from the Integration tab of the feed in your dashboard. It\nidentifies a feed; it does not authorise access — the secret key does that.\n",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    },
    "schemas": {
      "SearchRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "All fields optional. Every one narrows the result; none can widen it.",
        "properties": {
          "categories": {
            "type": "array",
            "maxItems": 8,
            "items": {
              "$ref": "#/components/schemas/Category"
            },
            "description": "Restrict to these categories. Categories your rules do not cover simply match nothing."
          },
          "costMinMinor": {
            "type": "integer",
            "minimum": 0,
            "description": "Minimum cost price, in cents, inclusive."
          },
          "costMaxMinor": {
            "type": "integer",
            "minimum": 0,
            "description": "Maximum cost price, in cents, inclusive."
          },
          "cursor": {
            "type": "integer",
            "minimum": 1,
            "description": "The `nextCursor` from the previous page."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 200,
            "default": 50
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "required": [
          "items",
          "nextCursor"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Product"
            }
          },
          "nextCursor": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Pass as `cursor` to fetch the next page. `null` means this was the last page —\nkeep paging only while it is non-null, and never assume a fixed page count.\n"
          }
        }
      },
      "Product": {
        "type": "object",
        "required": [
          "id",
          "title",
          "category",
          "tags",
          "costMinor",
          "retailMinor",
          "currency",
          "matchedRuleId",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Stable identifier for this product. Use it as your own catalogue key."
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "category": {
            "$ref": "#/components/schemas/Category"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "materialsNote": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free text from the designer, e.g. \"18k white gold\"."
          },
          "costMinor": {
            "type": "integer",
            "description": "What DYJ charges you to produce this piece, in cents. This is your cost, not a\nrecommended price, and it is confidential to your account.\n"
          },
          "retailMinor": {
            "type": "integer",
            "description": "Your selling price, in cents, computed from `costMinor` by the margin schedule of\nthe rule named in `matchedRuleId`:\n\n`retail = round_half_up(cost × (1 + percent/100)) + addon`\n\nNo other account can see it, and DYJ never charges it — it exists so your store and\nyour dashboard agree without you recomputing anything.\n"
          },
          "currency": {
            "type": "string",
            "examples": [
              "EUR"
            ],
            "description": "EUR only in v1."
          },
          "matchedRuleId": {
            "type": "integer",
            "description": "Which of your rules priced this product. Rules are ordered and the FIRST match\nwins, so this tells you why a product costs what it does.\n"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Category": {
        "type": "string",
        "enum": [
          "ring",
          "earrings",
          "bracelet",
          "necklace",
          "pendant",
          "brooch",
          "cufflinks",
          "other"
        ]
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A stable machine-readable code. Branch on this, not on the message."
          },
          "message": {
            "type": "string",
            "description": "Human-readable. May be reworded without notice."
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request body did not validate. Unknown fields are rejected rather than ignored —\na silently dropped filter would let you believe it applied.\n",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "message": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "error": {
                  "type": "string"
                },
                "statusCode": {
                  "type": "integer"
                }
              }
            },
            "examples": {
              "unknownField": {
                "value": {
                  "message": [
                    "property listingIds should not exist"
                  ],
                  "error": "Bad Request",
                  "statusCode": 400
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, malformed, unknown, or revoked key — all four answer identically, on purpose.\nThe remedy is the same in every case: check the key, or rotate it.\n",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "message": {
                  "type": "string",
                  "examples": [
                    "Invalid API key"
                  ]
                },
                "error": {
                  "type": "string"
                },
                "statusCode": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "Your DYJ plan is not live. The feed resumes the moment it is.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "lapsed": {
                "value": {
                  "error": "plan_required",
                  "message": "This account has no live plan"
                }
              }
            }
          }
        }
      },
      "FeedNotFound": {
        "description": "No such feed, or not yours, or disabled. Merged deliberately: an authenticated stranger\nmust not be able to learn that a feed id exists.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "notFound": {
                "value": {
                  "error": "feed_not_found",
                  "message": "Feed not found"
                }
              },
              "disabled": {
                "value": {
                  "error": "feed_disabled",
                  "message": "This feed is disabled"
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests for this key. Limits are per **key**, not per IP, so adding servers\ndoes not add quota. Back off and retry; a full catalogue sync should page steadily\nrather than fan out.\n",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "message": {
                  "type": "string",
                  "examples": [
                    "Rate limit exceeded"
                  ]
                },
                "statusCode": {
                  "type": "integer",
                  "examples": [
                    429
                  ]
                }
              }
            }
          }
        }
      }
    }
  }
}