{
  "openapi": "3.1.0",
  "info": {
    "title": "What I Wish public API",
    "version": "1.0.0",
    "summary": "Read a published gift list, reserve an item, and unfurl product links.",
    "description": "What I Wish is a free gift-list app: the owner creates a list, shares the link, and guests reserve items so nobody buys the same thing twice.\n\nEvery operation here is public: no account, no API key, no token. The `security` of this document is empty on purpose, and the one security scheme listed under components only exists to name the Firebase session that the app itself uses for owner-only endpoints, which are not part of this public API. Rate limits are per client IP, documented on each operation, and reported live in the RateLimit, RateLimit-Policy and X-RateLimit-* response headers (Retry-After on a 429).\n\nVersioning: the canonical paths are under /api/v1/. The unversioned /api/ paths are kept as aliases of the same functions. A breaking change ships as /api/v2/ and is announced in the changelog before v1 is retired. Human-readable docs: https://whatiwish.app/docs.",
    "contact": {
      "name": "What I Wish",
      "email": "hola@whatiwish.app",
      "url": "https://whatiwish.app/es/contacto"
    },
    "termsOfService": "https://whatiwish.app/es/terminos"
  },
  "servers": [
    {
      "url": "https://whatiwish.app",
      "description": "Production"
    }
  ],
  "security": [],
  "externalDocs": {
    "url": "https://whatiwish.app/docs",
    "description": "Developer docs"
  },
  "tags": [
    {
      "name": "wishlists",
      "description": "Published gift lists and their items."
    },
    {
      "name": "reservations",
      "description": "Reserve an item on a published list as a guest."
    },
    {
      "name": "links",
      "description": "Turn a product URL into a title, image and price."
    }
  ],
  "paths": {
    "/api/v1/public-wishlist/{slug}": {
      "get": {
        "operationId": "getPublicWishlist",
        "tags": [
          "wishlists"
        ],
        "summary": "Get a published gift list by its slug",
        "description": "Returns the list and its non-archived items for a list that the owner published. Draft or private lists answer 404, the same as a slug that does not exist. The slug is the last segment of the public URL https://whatiwish.app/w/{slug}. Limit: 60 requests per IP per minute.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Public slug of the list, as it appears in /w/{slug}.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 128
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The published list.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicWishlistResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing slug (code MISSING_SLUG).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No published list with that slug (code WISHLIST_NOT_FOUND or WISHLIST_NOT_PUBLISHED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "405": {
            "description": "Only GET is allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Per-IP quota exceeded (code RATE_LIMIT). Honor Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            }
          },
          "500": {
            "description": "Unexpected error (code INTERNAL).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/reserve": {
      "post": {
        "operationId": "reserveWishItem",
        "tags": [
          "reservations"
        ],
        "summary": "Reserve an item on a published list",
        "description": "Marks an item as reserved on behalf of a guest, without an account. The owner sees that the item is taken, not who took it. Limits: 10 reservations per IP every 10 minutes (429 above that). Anti-abuse: when Cloudflare Turnstile is enabled on the site, the request must carry a turnstileToken issued by the widget on the public list page; without it the API answers 400 (code MISSING_CAPTCHA). In practice that makes this operation callable from the list page, not from a headless client. The response includes a cancelToken; keep it, it is the only way to undo the reservation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReserveRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reservation created.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReserveResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body (code INVALID_BODY, with details), missing or failed CAPTCHA (MISSING_CAPTCHA, CAPTCHA_FAILED), or the item does not belong to that list (INVALID_WISHLIST).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Item or list not found, or list not published (code NOT_FOUND).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "405": {
            "description": "Only POST is allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "The item is already reserved (code ALREADY_RESERVED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Per-IP quota exceeded (code RATE_LIMIT). Honor Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            }
          },
          "500": {
            "description": "Unexpected error (code INTERNAL).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/unfurl": {
      "post": {
        "operationId": "unfurlUrl",
        "tags": [
          "links"
        ],
        "summary": "Get title, description, image and price for a URL",
        "description": "Fetches the page server-side and returns its Open Graph and product metadata. Private and internal addresses are rejected (400). Limit: 10 requests per IP per minute.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UrlRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Metadata found for the URL. success is false when the page could not be read; the error field explains why.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnfurlResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing URL (code MISSING_URL) or URL not allowed (code INVALID_URL).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "405": {
            "description": "Only POST is allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Per-IP quota exceeded (code RATE_LIMIT). Honor Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            }
          },
          "500": {
            "description": "The page could not be fetched (code FETCH_FAILED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scrape-product": {
      "post": {
        "operationId": "scrapeProduct",
        "tags": [
          "links"
        ],
        "summary": "Extract a product name, image and price from a store URL",
        "description": "Like unfurl, tuned for product pages: it also reads JSON-LD and store-specific price markup. Returns 422 when nothing product-like was found. Private and internal addresses are rejected (400). Limit: 10 requests per IP per minute.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UrlRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Product data found.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScrapeProductResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing URL (code MISSING_URL) or URL not allowed (code INVALID_URL).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "405": {
            "description": "Only POST is allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "422": {
            "description": "No product data on that page (code NO_PRODUCT_DATA).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "Per-IP quota exceeded (code RATE_LIMIT). Honor Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            }
          },
          "500": {
            "description": "The page could not be fetched (code FETCH_FAILED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "firebaseSession": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Firebase ID token of a signed-in list owner. No operation in this document requires it, and there is no way to obtain one from the API: it only exists to name the credential the app uses for owner-only endpoints (creating and editing lists), which are deliberately not part of the public API. Scopes: none; the public API has a single, unauthenticated permission level: read published lists, reserve on them, and unfurl links."
      }
    },
    "headers": {
      "RateLimitPolicy": {
        "description": "Quota policy for this endpoint: \"<name>\";q=<requests>;w=<window seconds> (IETF RateLimit header fields draft).",
        "schema": {
          "type": "string",
          "examples": [
            "\"per-ip\";q=10;w=60"
          ]
        }
      },
      "RateLimit": {
        "description": "Current quota: \"<name>\";r=<remaining>;t=<seconds until reset>.",
        "schema": {
          "type": "string",
          "examples": [
            "\"per-ip\";r=7;t=42"
          ]
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying. Only on 429.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "schemas": {
      "ApiError": {
        "type": "object",
        "description": "Every error is JSON with this shape, including unknown /api paths (404) and wrong methods (405).",
        "required": [
          "success",
          "error",
          "docs"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string",
            "description": "Human-readable message."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable code, for example RATE_LIMIT or NOT_FOUND."
          },
          "hint": {
            "type": "string",
            "description": "What to change to get a successful response."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "Link to the developer docs."
          },
          "details": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Validation messages, only on INVALID_BODY."
          }
        }
      },
      "UrlRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Public http(s) URL of the page. Text around the URL is tolerated; the first URL in it is used."
          }
        }
      },
      "ReserveRequest": {
        "type": "object",
        "required": [
          "wishItemId",
          "wishlistId"
        ],
        "properties": {
          "wishItemId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "Item id, from items[].id of the public list."
          },
          "wishlistId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "List id, from wishlist.id of the public list."
          },
          "reservedByName": {
            "type": "string",
            "maxLength": 80,
            "description": "Guest name. Optional; shown to nobody but the guest."
          },
          "reservedByEmail": {
            "type": "string",
            "format": "email",
            "maxLength": 254,
            "description": "Guest email, used to send the cancel link. Optional."
          },
          "turnstileToken": {
            "type": "string",
            "description": "Cloudflare Turnstile token from the list page widget. Required when Turnstile is enabled on the site."
          },
          "marketingOptIn": {
            "type": "boolean",
            "description": "Requires reservedByEmail."
          }
        }
      },
      "ReserveResponse": {
        "type": "object",
        "required": [
          "success",
          "reservationId",
          "cancelToken"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "reservationId": {
            "type": "string"
          },
          "cancelToken": {
            "type": "string",
            "description": "Secret token that lets the guest undo the reservation at /cancel."
          }
        }
      },
      "PublicWishlist": {
        "type": "object",
        "required": [
          "id",
          "title"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "customSlug": {
            "type": "string"
          },
          "occasion": {
            "type": "string",
            "description": "birthday, wedding, baby_shower, christmas, mothers_day, fathers_day or other."
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "coverUrl": {
            "type": "string",
            "format": "uri"
          },
          "theme": {
            "type": "string"
          },
          "isSurpriseMode": {
            "type": "boolean",
            "description": "When true the owner cannot see who reserved what."
          },
          "visibility": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "eventDate": {
            "type": "string",
            "format": "date-time"
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "reservedCount": {
            "type": "integer"
          },
          "itemCount": {
            "type": "integer"
          },
          "viewCount": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PublicWishItem": {
        "type": "object",
        "required": [
          "id",
          "wishlistId",
          "title"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "wishlistId": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Store link the owner added, if any."
          },
          "price": {
            "type": "number"
          },
          "currency": {
            "type": "string"
          },
          "imageUrl": {
            "type": "string",
            "format": "uri"
          },
          "notes": {
            "type": "string"
          },
          "order": {
            "type": "integer"
          },
          "archived": {
            "type": "boolean"
          },
          "reservationStatus": {
            "type": "string",
            "description": "available or reserved."
          },
          "isGroupGift": {
            "type": "boolean"
          },
          "targetAmount": {
            "type": "number"
          },
          "contributions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "amount": {
                  "type": "number"
                },
                "addedAt": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "originalPrice": {
            "type": "number"
          },
          "lowestPrice": {
            "type": "number"
          },
          "lastPrice": {
            "type": "number"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PublicWishlistResponse": {
        "type": "object",
        "required": [
          "success",
          "wishlist",
          "items"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "wishlist": {
            "$ref": "#/components/schemas/PublicWishlist"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicWishItem"
            }
          },
          "ownerName": {
            "type": [
              "string",
              "null"
            ]
          },
          "ownerUsername": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "UnfurlResponse": {
        "type": "object",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "image": {
            "type": "string",
            "format": "uri"
          },
          "price": {
            "type": "number"
          },
          "currency": {
            "type": "string"
          },
          "siteName": {
            "type": "string"
          },
          "error": {
            "type": "string",
            "description": "Present when success is false."
          }
        }
      },
      "ScrapeProductResponse": {
        "type": "object",
        "required": [
          "success",
          "sourceUrl"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "name": {
            "type": "string"
          },
          "image": {
            "type": "string",
            "format": "uri"
          },
          "price": {
            "type": "number"
          },
          "currency": {
            "type": "string"
          },
          "sourceUrl": {
            "type": "string",
            "format": "uri",
            "description": "The URL after normalization."
          }
        }
      }
    }
  }
}