{
  "openapi": "3.1.0",
  "info": {
    "title": "Ranklify API",
    "version": "1.1.0",
    "description": "Generate platform-optimized product listings for Amazon, Shopify, and WooCommerce. Two token types: API keys (scope 'api', Agency plan only, created at /api-keys) and store connections (scope 'plugin', issued by the connect flow, valid on every plan including Free). Both use Bearer auth and share the account's monthly credit pool. Full docs: https://www.getranklify.com/developers",
    "contact": { "name": "Ranklify Support", "url": "https://www.getranklify.com/contact" }
  },
  "servers": [
    { "url": "https://www.getranklify.com/api/v1", "description": "Production (always use the www host)" }
  ],
  "security": [{ "bearerAuth": [] }],
  "paths": {
    "/generate": {
      "post": {
        "summary": "Generate a listing (synchronous)",
        "description": "Generates a single optimized product listing and files it under the account (optionally a specific brand workspace). The request stays open for the whole generation (15-35s), so use a client timeout of at least 60s. Clients with a hard execution cap (PHP, WordPress, serverless) should use POST /jobs instead. Rate limited to 10 requests/min per token. Each success counts against the plan's monthly quota.",
        "operationId": "generateListing",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/GenerateRequest" },
              "example": {
                "product_name": "Stainless Steel Pour-Over Kettle",
                "features": "Gooseneck spout, 1L, food-grade steel, keeps heat",
                "platform": "amazon",
                "tone": "Professional"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Listing generated",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GenerateResponse" } } }
          },
          "400": { "description": "invalid_json or validation_error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "missing_authorization or invalid_api_key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "plan_required, scope_forbidden, brand_forbidden, or usage_limit_reached", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "410": { "description": "brand_archived", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "rate_limited", "headers": { "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds to wait" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "internal_error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/jobs": {
      "post": {
        "summary": "Queue a listing generation (asynchronous)",
        "description": "Queues a generation and returns 202 immediately with a job_id. Poll GET /jobs/{id} until status is 'complete' or 'failed'. No request stays open longer than a second, which makes this safe inside WordPress, cron workers, and anywhere else with an execution cap. A credit is reserved on creation and refunded exactly once if the job ends up failed.",
        "operationId": "createJob",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": { "type": "string", "maxLength": 200 },
            "description": "Strongly recommended. A retry carrying the same key returns the original job instead of queueing a second generation. Scoped to the account, remembered for 24 hours. A deduped replay costs no credit and omits the usage object."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/JobRequest" },
              "example": {
                "product_name": "Stainless Steel Pour-Over Kettle",
                "features": "Gooseneck spout, 1L, food-grade steel, keeps heat",
                "platform": "woocommerce",
                "external_ref": "4471"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job queued",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } }
          },
          "400": { "description": "invalid_json or validation_error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "missing_authorization or invalid_api_key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "plan_required, scope_forbidden, brand_forbidden, or usage_limit_reached", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "410": { "description": "brand_archived", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "rate_limited", "headers": { "Retry-After": { "schema": { "type": "integer" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "internal_error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "get": {
        "summary": "List recent jobs",
        "description": "Returns the account's most recent jobs, newest first. Exempt from the rate limit.",
        "operationId": "listJobs",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 } }
        ],
        "responses": {
          "200": {
            "description": "Recent jobs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "api_version": { "type": "string", "example": "v1" },
                    "jobs": { "type": "array", "items": { "$ref": "#/components/schemas/Job" } }
                  }
                }
              }
            }
          },
          "401": { "description": "missing_authorization or invalid_api_key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "internal_error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/jobs/{id}": {
      "get": {
        "summary": "Poll a job",
        "description": "Returns the current state of a job. Poll every poll_after_ms (3000) until status is 'complete' or 'failed'. Exempt from the rate limit, so a 3-second loop is fine. Note that a failed job returns HTTP 200 with status: 'failed' — check the status field, not just the HTTP code.",
        "operationId": "getJob",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": {
            "description": "Job state (queued, processing, complete, or failed)",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } }
          },
          "401": { "description": "missing_authorization or invalid_api_key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "job_not_found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "internal_error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/me": {
      "get": {
        "summary": "Account & usage snapshot",
        "description": "Plan, remaining credits, what this token is, and which brands it can file listings under. Cheap and exempt from the rate limit — safe to call on every settings-screen render.",
        "operationId": "getMe",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Account snapshot",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Me" } } }
          },
          "401": { "description": "missing_authorization or invalid_api_key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "internal_error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/connect/exchange": {
      "post": {
        "summary": "Exchange a connect code for a store token",
        "description": "Server-to-server only. Trades the one-time ranklify_code returned to your return_url for a plugin token. Codes expire in 10 minutes and are single-use; the token is shown once and stored hashed. Connecting a store that is already connected revokes the previous token for that origin. No Bearer auth — possession of a valid code IS the credential.",
        "operationId": "exchangeConnectCode",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["code", "site"],
                "properties": {
                  "code": { "type": "string", "description": "The ranklify_code query parameter we redirected back with." },
                  "site": { "type": "string", "format": "uri", "description": "Must match the origin the code was issued for." }
                }
              },
              "example": { "code": "ONE_TIME_CODE", "site": "https://shop.example.com" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token issued (shown once)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "api_version": { "type": "string", "example": "v1" },
                    "token": { "type": "string", "description": "Store this. It is never shown again." },
                    "token_prefix": { "type": "string" },
                    "site_url": { "type": "string" },
                    "platform": { "type": "string", "enum": ["woocommerce"] },
                    "account": { "type": "object", "properties": { "email": { "type": ["string", "null"] }, "plan": { "type": "string" } } },
                    "usage": { "$ref": "#/components/schemas/Usage" }
                  }
                }
              }
            }
          },
          "400": { "description": "validation_error, invalid_code, or site_mismatch", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "rate_limited", "headers": { "Retry-After": { "schema": { "type": "integer" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "internal_error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Sent as `Authorization: Bearer lfy_...`. Either an API key from /api-keys (Agency plan required) or a store connection token from the connect flow (valid on every plan, including Free)."
      }
    },
    "schemas": {
      "GenerateRequest": {
        "type": "object",
        "required": ["product_name", "features"],
        "properties": {
          "product_name": { "type": "string", "maxLength": 200 },
          "features": { "type": "string", "maxLength": 600, "description": "Key features / selling points." },
          "platform": { "type": "string", "enum": ["amazon", "shopify", "woocommerce"], "default": "amazon" },
          "tone": { "type": "string", "enum": ["Professional", "Energetic", "Friendly", "Luxury", "Technical", "Playful", "Bold", "Minimalist"], "default": "Professional" },
          "category": { "type": "string", "maxLength": 100 },
          "price": { "type": "string", "maxLength": 100, "description": "Free-text price context, not a number." },
          "customer": { "type": "string", "maxLength": 500, "description": "Target customer & use cases." },
          "competitors": { "type": "string", "maxLength": 300, "description": "Competitor URLs or market context." },
          "brand_id": { "type": "string", "description": "Workspace/brand to file the listing under. Defaults to the active brand." }
        }
      },
      "JobRequest": {
        "allOf": [
          { "$ref": "#/components/schemas/GenerateRequest" },
          {
            "type": "object",
            "properties": {
              "external_ref": { "type": "string", "maxLength": 200, "description": "Your own identifier (e.g. a WooCommerce product ID). Echoed back on every poll so you can match a job to a row in your database." }
            }
          }
        ]
      },
      "Job": {
        "type": "object",
        "description": "One envelope for create and poll. Fields present depend on status: queued/processing carry poll_after_ms, complete carries listing + listing_id, failed carries error.",
        "properties": {
          "api_version": { "type": "string", "example": "v1" },
          "job_id": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["queued", "processing", "complete", "failed"] },
          "platform": { "type": "string", "enum": ["amazon", "shopify", "woocommerce"] },
          "external_ref": { "type": ["string", "null"] },
          "brand_id": { "type": ["string", "null"] },
          "created_at": { "type": "string", "format": "date-time" },
          "completed_at": { "type": "string", "format": "date-time" },
          "poll_url": { "type": "string", "description": "Present on create." },
          "poll_after_ms": { "type": "integer", "example": 3000, "description": "Present while queued or processing." },
          "listing": { "$ref": "#/components/schemas/Listing" },
          "listing_id": { "type": "string" },
          "error": {
            "type": "object",
            "description": "Present when status is 'failed'. The reserved credit has been refunded.",
            "properties": {
              "code": { "type": "string", "enum": ["generation_failed", "job_expired"] },
              "message": { "type": "string" }
            }
          },
          "usage": { "$ref": "#/components/schemas/Usage" }
        }
      },
      "Me": {
        "type": "object",
        "properties": {
          "api_version": { "type": "string", "example": "v1" },
          "account": {
            "type": "object",
            "properties": {
              "email": { "type": "string" },
              "plan": { "type": "string", "enum": ["free", "pro", "agency"] },
              "is_member": { "type": "boolean", "description": "True when this token belongs to a team member; the plan and pool reported are the workspace owner's." }
            }
          },
          "usage": { "$ref": "#/components/schemas/Usage" },
          "token": {
            "type": "object",
            "properties": {
              "scope": { "type": "string", "enum": ["api", "plugin"] },
              "name": { "type": "string" },
              "site_url": { "type": ["string", "null"] },
              "platform": { "type": ["string", "null"] }
            }
          },
          "brands": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string", "description": "Pass as brand_id when generating." },
                "name": { "type": "string" },
                "locked": { "type": "boolean", "description": "Exists but can't be written to on the current plan." }
              }
            }
          },
          "capabilities": {
            "type": "object",
            "properties": {
              "generate": { "type": "boolean" },
              "async_jobs": { "type": "boolean" },
              "bulk": { "type": "boolean" }
            }
          },
          "upgrade_url": { "type": "string", "format": "uri" }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "used": { "type": "integer" },
          "limit": { "type": "integer" },
          "plan": { "type": "string" },
          "remaining": { "type": "integer" }
        }
      },
      "GenerateResponse": {
        "type": "object",
        "properties": {
          "api_version": { "type": "string", "example": "v1" },
          "listing": { "$ref": "#/components/schemas/Listing" },
          "listing_id": { "type": "string" },
          "platform": { "type": "string", "enum": ["amazon", "shopify", "woocommerce"] },
          "tone": { "type": "string" },
          "brand_id": { "type": ["string", "null"] },
          "usage": {
            "type": "object",
            "properties": {
              "used": { "type": "integer" },
              "limit": { "type": "integer" },
              "plan": { "type": "string" }
            }
          }
        }
      },
      "Listing": {
        "type": "object",
        "description": "Stable superset. Every key is always present; fields not applicable to the chosen platform are empty (\"\" or []).",
        "properties": {
          "title": { "type": "string" },
          "bullets": { "type": "array", "items": { "type": "string" }, "description": "Up to 5. Amazon + WooCommerce." },
          "description": { "type": "string" },
          "keywords": { "type": "array", "items": { "type": "string" }, "description": "Up to 10." },
          "backend_terms": { "type": "string", "description": "Amazon only." },
          "focus_keyphrase": { "type": "string", "description": "WooCommerce only." },
          "meta_description": { "type": "string", "description": "Shopify + WooCommerce." },
          "product_tags": { "type": "array", "items": { "type": "string" }, "description": "Up to 8. WooCommerce only." },
          "seo_scores": {
            "type": "object",
            "properties": {
              "keyword_density": { "type": "integer", "minimum": 0, "maximum": 100 },
              "readability": { "type": "integer", "minimum": 0, "maximum": 100 },
              "conversion_potential": { "type": "integer", "minimum": 0, "maximum": 100 },
              "platform_compliance": { "type": "integer", "minimum": 0, "maximum": 100 }
            }
          },
          "pro_tip": { "type": "string" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": ["missing_authorization", "invalid_api_key", "plan_required", "scope_forbidden", "brand_forbidden", "brand_archived", "invalid_json", "validation_error", "usage_limit_reached", "job_not_found", "invalid_code", "site_mismatch", "rate_limited", "internal_error"]
              },
              "message": { "type": "string" }
            }
          }
        }
      }
    }
  }
}
