{
  "openapi": "3.1.0",
  "info": {
    "title": "foto foto connector",
    "version": "2.0.0",
    "description": "Create editable photo books, review prices and prepare purchases. Full access requirements: https://fotofoto.app/connectors.md. Versioning: paths carry no version segment; a breaking change is published under a new base path and the previous path sends Deprecation and Sunset headers for at least 90 days before it stops. Idempotency: every write takes an idempotencyKey in its body; reuse it with the same inputs to retry safely. Rate limits: every response carries RateLimit-Policy, and 429 responses carry Retry-After.",
    "contact": {
      "name": "foto foto support",
      "email": "fotofoto@nternet.company",
      "url": "https://fotofoto.app/help/support"
    }
  },
  "servers": [
    {
      "url": "https://fotofoto.app"
    }
  ],
  "security": [],
  "paths": {
    "/api/connector/catalog": {
      "get": {
        "operationId": "getCatalog",
        "description": "List active products, the price list of the default product's materials, shipping countries and upload requirements.",
        "responses": {
          "200": {
            "description": "Result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "400": {
            "description": "Invalid input; details lists the fields",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate or daily quota exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "503": {
            "description": "Outcome uncertain; retry with the same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/prices": {
      "get": {
        "operationId": "comparePhotoBookPrices",
        "description": "What one photo book costs at foto foto and at every other service in the comparison that sells it, cheapest first, with the page each price was read from. List prices in EUR with VAT, without shipping or discounts. Every parameter is optional, and a missing or unknown value reads as the standard book: standard binding, smooth cover, glossy paper, 74 pages. Public, no book token, cacheable for 5 minutes. The MCP tool compare_photo_book_prices returns the same result.",
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "binding",
            "description": "perfect is the standard binding, where the pages curve towards the spine; layflat opens completely flat.",
            "schema": {
              "type": "string",
              "enum": ["perfect", "layflat"],
              "default": "perfect"
            }
          },
          {
            "in": "query",
            "name": "cover",
            "description": "smooth is the standard hardcover; linen is a linen hardcover.",
            "schema": {
              "type": "string",
              "enum": ["smooth", "linen"],
              "default": "smooth"
            }
          },
          {
            "in": "query",
            "name": "paper",
            "schema": {
              "type": "string",
              "enum": ["glossy", "matte", "fine-art"],
              "default": "glossy"
            }
          },
          {
            "in": "query",
            "name": "pages",
            "description": "Printed pages, an even number from 24 to 300, layflat up to 120. An odd count is rounded up and a count outside the range is moved inside it.",
            "schema": {
              "type": "integer",
              "default": 74
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The book priced at foto foto and at every service in the comparison",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PriceComparison"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          }
        }
      }
    },
    "/api/connector/photos": {
      "post": {
        "operationId": "uploadPhoto",
        "description": "Upload a JPEG, PNG or WebP original, at most 15 MiB. Returns a private uploadId valid for 24 hours. No login required.",
        "responses": {
          "201": {
            "description": "Result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "400": {
            "description": "Invalid input; details lists the fields",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Photo over 15 MiB or 60 megapixels",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate or daily quota exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "503": {
            "description": "Outcome uncertain; retry with the same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        }
      }
    },
    "/api/connector/books": {
      "post": {
        "operationId": "createBook",
        "description": "Create a free editable book. Photos are used in supplied order. Instead of photos, pass a public iCloud Shared Album link as sharedAlbumUrl, or ChatGPT attachments. Exactly one source per call. Reuse idempotencyKey for retries.",
        "responses": {
          "200": {
            "description": "Result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "400": {
            "description": "Invalid input; details lists the fields",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Same idempotency key with different inputs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Photo over 15 MiB or 60 megapixels, or more than 100 photos",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate or daily quota exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "503": {
            "description": "Outcome uncertain; retry with the same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["title", "idempotencyKey"],
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  "productId": {
                    "type": "string"
                  },
                  "photos": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "uploadId": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "filename": {
                          "type": "string",
                          "maxLength": 200
                        }
                      },
                      "oneOf": [
                        {
                          "required": ["url"]
                        },
                        {
                          "required": ["uploadId"]
                        }
                      ],
                      "additionalProperties": false
                    }
                  },
                  "idempotencyKey": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Fresh random UUID. Keep private. Reuse for retries."
                  },
                  "sharedAlbumUrl": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048,
                    "description": "Public iCloud Shared Album link, e.g. https://www.icloud.com/sharedalbum/#TOKEN. Used instead of photos: largest version of each photo, videos skipped, date order, first 100."
                  },
                  "attachments": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "description": "Photos attached in ChatGPT (Apps SDK file parameter). Used instead of photos or sharedAlbumUrl.",
                    "items": {
                      "type": "object",
                      "required": ["download_url", "file_id"],
                      "properties": {
                        "download_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "file_id": {
                          "type": "string"
                        },
                        "mime_type": {
                          "type": "string"
                        },
                        "file_name": {
                          "type": "string",
                          "maxLength": 200
                        }
                      }
                    }
                  }
                },
                "oneOf": [
                  {
                    "required": ["photos"]
                  },
                  {
                    "required": ["sharedAlbumUrl"]
                  },
                  {
                    "required": ["attachments"]
                  }
                ]
              }
            }
          }
        },
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": false,
            "description": "Alias of the idempotencyKey body field for clients that send the key as a header. The body field wins when both are present.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/connector/book": {
      "get": {
        "operationId": "getBook",
        "description": "Get an owned book. Include country for a shipping-inclusive EUR price. Unorderable books have price=null and an explanation.",
        "responses": {
          "200": {
            "description": "Result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "400": {
            "description": "Invalid input; details lists the fields",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Book token missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Book token wrong for this book",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Book not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate or daily quota exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "503": {
            "description": "Outcome uncertain; retry with the same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "in": "query",
            "name": "bookId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "country",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 2
            }
          },
          {
            "in": "header",
            "name": "X-Fotofoto-Book-Token",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/connector/checkout": {
      "post": {
        "operationId": "createCheckout",
        "description": "Return a Stripe hosted checkout link. This does not take payment. The buyer confirms the final amount and delivery address at Stripe.",
        "responses": {
          "200": {
            "description": "Result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "400": {
            "description": "Invalid input; details lists the fields",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Book token missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Book token wrong for this book",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Book not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Same idempotency key with different inputs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "Book not claimed or too short to print",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate or daily quota exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "503": {
            "description": "Outcome uncertain; retry with the same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["bookId", "country", "idempotencyKey", "bookToken"],
                "properties": {
                  "bookId": {
                    "type": "string"
                  },
                  "country": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 2
                  },
                  "quantity": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10,
                    "default": 1
                  },
                  "idempotencyKey": {
                    "type": "string",
                    "minLength": 8,
                    "maxLength": 100,
                    "pattern": "^[a-zA-Z0-9_-]+$",
                    "description": "Client-generated key, 8 to 100 characters. Reuse it with the same inputs to retry; the same key with different inputs is a 409."
                  },
                  "bookToken": {
                    "type": "string",
                    "pattern": "^[a-f0-9]{64}$"
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": false,
            "description": "Alias of the idempotencyKey body field for clients that send the key as a header. The body field wins when both are present.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/connector/quotes": {
      "post": {
        "operationId": "createPaymentQuote",
        "description": "Optional MPP: freeze a book snapshot, address and price for 15 minutes. Requires merchant enablement. Ask the buyer to confirm the exact total and address before paying.",
        "responses": {
          "200": {
            "description": "Result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "400": {
            "description": "Invalid input; details lists the fields",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Book token missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Book token wrong for this book",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Book not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Same idempotency key with different inputs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "Book not claimed, quote expired or payment configuration unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate or daily quota exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "503": {
            "description": "Outcome uncertain; retry with the same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "bookId",
                  "country",
                  "idempotencyKey",
                  "shippingAddress",
                  "bookToken"
                ],
                "properties": {
                  "bookId": {
                    "type": "string"
                  },
                  "country": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 2
                  },
                  "quantity": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10,
                    "default": 1
                  },
                  "idempotencyKey": {
                    "type": "string",
                    "minLength": 8,
                    "maxLength": 100,
                    "pattern": "^[a-zA-Z0-9_-]+$",
                    "description": "Client-generated key, 8 to 100 characters. Reuse it with the same inputs to retry; the same key with different inputs is a 409."
                  },
                  "shippingAddress": {
                    "type": "object",
                    "required": [
                      "firstName",
                      "lastName",
                      "addressLine1",
                      "city",
                      "postalCode",
                      "phone"
                    ],
                    "properties": {
                      "firstName": {
                        "type": "string"
                      },
                      "lastName": {
                        "type": "string"
                      },
                      "addressLine1": {
                        "type": "string"
                      },
                      "addressLine2": {
                        "type": "string"
                      },
                      "city": {
                        "type": "string"
                      },
                      "state": {
                        "type": "string"
                      },
                      "postalCode": {
                        "type": "string"
                      },
                      "phone": {
                        "type": "string"
                      }
                    }
                  },
                  "bookToken": {
                    "type": "string",
                    "pattern": "^[a-f0-9]{64}$"
                  }
                }
              }
            }
          }
        },
        "x-payment-info": {
          "protocols": ["MPP", "x402 v1"],
          "next": "POST /api/connector/pay/{quoteId}",
          "x402": "The response's x402 object carries amount (USDC, 6 decimals), network, asset and payTo when x402 is on"
        },
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": false,
            "description": "Alias of the idempotencyKey body field for clients that send the key as a header. The body field wins when both are present.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/connector/pay/{quoteId}": {
      "post": {
        "operationId": "payQuote",
        "description": "Pay a quote. Without a credential the response is 402 carrying the MPP challenge (WWW-Authenticate: Payment) and the x402 v1 requirements (JSON body, accepts[]). Retry with Authorization: Payment <credential> for MPP, or X-PAYMENT: <base64 x402 payload> for USDC on Base. Reuse the same credential on retry; a settled quote returns the same receipt.",
        "responses": {
          "200": {
            "description": "Result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-PAYMENT-RESPONSE": {
                "description": "Present after an x402 payment: base64 JSON {success, transaction, network, payer}.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input; details lists the fields",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Book token missing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Payment required: MPP challenge in WWW-Authenticate and x402 requirements in the body. After an invalid x402 payment the body's error names the facilitator's reason.",
            "headers": {
              "WWW-Authenticate": {
                "schema": {
                  "type": "string"
                },
                "description": "Payment challenge (Machine Payments Protocol)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["x402Version", "accepts"],
                  "properties": {
                    "x402Version": {
                      "type": "integer",
                      "enum": [1]
                    },
                    "error": {
                      "type": "string"
                    },
                    "accepts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "scheme": {
                            "type": "string",
                            "enum": ["exact"]
                          },
                          "network": {
                            "type": "string",
                            "example": "base"
                          },
                          "maxAmountRequired": {
                            "type": "string",
                            "description": "USDC in 6-decimal units"
                          },
                          "resource": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          },
                          "mimeType": {
                            "type": "string"
                          },
                          "payTo": {
                            "type": "string"
                          },
                          "maxTimeoutSeconds": {
                            "type": "integer"
                          },
                          "asset": {
                            "type": "string"
                          },
                          "extra": {
                            "type": "object",
                            "properties": {
                              "name": {
                                "type": "string"
                              },
                              "version": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Book token wrong for this book",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Book not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Payment already attempted or awaiting resolution; do not submit a new payment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "Quote expired, book not claimed or payment configuration unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate or daily quota exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "503": {
            "description": "MPP is not configured, or the outcome is uncertain; use Stripe Checkout or retry with the same quote",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "in": "path",
            "name": "quoteId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "header",
            "name": "Authorization",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "header",
            "name": "X-Fotofoto-Book-Token",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "header",
            "name": "X-PAYMENT",
            "required": false,
            "description": "x402 v1 payment payload, base64 JSON: {x402Version:1, scheme:\"exact\", network, payload:{signature, authorization:{from,to,value,validAfter,validBefore,nonce}}}.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "x-payment-info": {
          "protocols": ["Machine Payments Protocol (MPP)", "x402 v1"],
          "mpp": {
            "challenge": "HTTP 402 with WWW-Authenticate: Payment; retry with Authorization: Payment <credential>",
            "method": "Stripe Shared Payment Token",
            "currency": "EUR"
          },
          "x402": {
            "version": 1,
            "scheme": "exact",
            "network": "base",
            "asset": "USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
            "facilitator": "Coinbase",
            "challenge": "HTTP 402 JSON body accepts[]; retry with X-PAYMENT",
            "receipt": "X-PAYMENT-RESPONSE header",
            "amount": "quote.x402.amount, fixed with the quote at the ECB EUR/USD rate"
          },
          "quote": "POST /api/connector/quotes freezes book, quantity, address, EUR total and USDC amount for 15 minutes",
          "documentation": "https://fotofoto.app/connectors.md#payments"
        }
      }
    },
    "/api/connector/invitations": {
      "post": {
        "operationId": "createPhotoInvitation",
        "description": "Create a private partner claim link. Stores photo URLs but fetches no images until the recipient opens the link. No account required. Email verification establishes ownership after import.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["title", "idempotencyKey", "photos"],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "idempotencyKey": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "photos": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "type": "object",
                      "required": ["url"],
                      "properties": {
                        "url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "filename": {
                          "type": "string",
                          "maxLength": 200
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Private claimUrl and expiry. No images fetched.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "400": {
            "description": "Unsupported source or invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Same idempotency key with different inputs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate or daily quota exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            }
          },
          "503": {
            "description": "Outcome uncertain; retry with the same idempotency key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": false,
            "description": "Alias of the idempotencyKey body field for clients that send the key as a header. The body field wins when both are present.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    }
  },
  "externalDocs": {
    "description": "Connector documentation",
    "url": "https://fotofoto.app/connectors.md"
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "Machine-readable code, e.g. BAD_REQUEST, NOT_FOUND, CONFLICT, PRECONDITION_FAILED, PAYLOAD_TOO_LARGE, TOO_MANY_REQUESTS, or a fixed phrase such as \"Invalid request\"."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation with the next step."
          },
          "details": {
            "type": "array",
            "description": "Validation issues, present on 400 responses from schema validation.",
            "items": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "array",
                  "items": {
                    "type": ["string", "integer"]
                  }
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        },
        "additionalProperties": false
      },
      "PriceList": {
        "type": "object",
        "description": "foto foto's price for every material, in EUR with VAT, without shipping. A book costs startPrice[binding] + coverSurcharge[cover] + max(0, pages - includedPages) * pricePerExtraPage[binding][paper].",
        "properties": {
          "currency": {
            "type": "string"
          },
          "vatIncluded": {
            "type": "boolean"
          },
          "shippingIncluded": {
            "type": "boolean"
          },
          "formula": {
            "type": "string"
          },
          "includedPages": {
            "type": "integer"
          },
          "pageStep": {
            "type": "integer"
          },
          "startPrice": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "Per binding."
          },
          "coverSurcharge": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "Per cover, added once."
          },
          "pricePerExtraPage": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "type": "number"
              }
            },
            "description": "Per binding, then per paper."
          },
          "maxPages": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Per binding."
          }
        }
      },
      "PriceRow": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "book": {
            "type": "string",
            "description": "The service's own name for the book, in English."
          },
          "pages": {
            "type": "integer",
            "description": "The page count it is priced at: the first count the service sells at or above the asked count."
          },
          "price": {
            "type": ["number", "null"],
            "description": "List price in EUR with VAT; null when the service cannot make the book this long."
          },
          "maxPages": {
            "type": "integer"
          },
          "source": {
            "type": "string",
            "format": "uri",
            "description": "The service's own page the price was read from."
          },
          "layflatInstead": {
            "type": "boolean",
            "description": "The service only makes layflat books, so its layflat book is priced."
          }
        }
      },
      "PriceComparison": {
        "type": "object",
        "properties": {
          "currency": {
            "type": "string",
            "example": "EUR"
          },
          "vatIncluded": {
            "type": "boolean"
          },
          "shippingIncluded": {
            "type": "boolean"
          },
          "discountsIncluded": {
            "type": "boolean"
          },
          "checked": {
            "type": "string",
            "format": "date",
            "description": "The date the prices were checked."
          },
          "configuration": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "example": "layflat-linen-fine-art"
              },
              "binding": {
                "type": "string"
              },
              "cover": {
                "type": "string"
              },
              "paper": {
                "type": "string"
              }
            }
          },
          "pages": {
            "type": "integer",
            "description": "The page count everything is priced at."
          },
          "answer": {
            "type": "string",
            "description": "Where this book is cheapest at this page count, in one or two sentences."
          },
          "ranking": {
            "type": "array",
            "description": "Everyone who sells this book at this length, foto foto included, cheapest first.",
            "items": {
              "$ref": "#/components/schemas/RankedPrice"
            }
          },
          "ours": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "price": {
                "type": "number"
              },
              "startPrice": {
                "type": "number"
              },
              "includedPages": {
                "type": "integer"
              },
              "pricePerExtraPage": {
                "type": "number"
              },
              "pageStep": {
                "type": "integer"
              },
              "maxPages": {
                "type": "integer"
              }
            }
          },
          "others": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PriceRow"
            },
            "description": "Services that sell this book at this length, cheapest first."
          },
          "tooLong": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PriceRow"
            },
            "description": "Services that sell it, but not with this many pages."
          },
          "notSold": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Services that do not sell this book."
          },
          "unpublished": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Services that sell it but show the price only inside their editor."
          },
          "rank": {
            "type": "integer",
            "description": "foto foto's place among all prices, 1 is cheapest."
          },
          "of": {
            "type": "integer"
          },
          "calculatorUrl": {
            "type": "string",
            "format": "uri"
          },
          "priceList": {
            "$ref": "#/components/schemas/PriceList"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "RankedPrice": {
        "type": "object",
        "properties": {
          "place": {
            "type": "integer",
            "description": "1 is cheapest."
          },
          "name": {
            "type": "string"
          },
          "book": {
            "type": "string",
            "description": "The service's own name for the book, in English."
          },
          "pages": {
            "type": "integer",
            "description": "The page count it is priced at."
          },
          "price": {
            "type": ["number", "null"],
            "description": "List price in euros with VAT."
          },
          "shippingNl": {
            "type": ["number", "null"],
            "description": "Shipping to the Netherlands in euros."
          },
          "shippingNote": {
            "type": ["string", "null"],
            "description": "A condition on that shipping price, if the service states one."
          },
          "totalNl": {
            "type": ["number", "null"],
            "description": "price + shippingNl."
          },
          "source": {
            "type": "string",
            "description": "The page the price was read from."
          }
        }
      }
    },
    "headers": {
      "RateLimit-Policy": {
        "description": "draft-ietf-httpapi-ratelimit-headers policy of the edge limiter: 60 requests per 60 seconds per IP.",
        "schema": {
          "type": "string",
          "example": "\"connector\";q=60;w=60"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "example": 60
        }
      }
    }
  }
}
