{
  "openapi": "3.0.3",
  "info": {
    "title": "MedshotsAI Agent API",
    "version": "1.0.0",
    "description": "Turn a few ordinary photos of a person into a professional headshot — for LinkedIn, a CV or resume, a company team page, a speaker bio, or a hospital directory.\n\nThis is a narrow, agent-facing surface, separate from the app's internal API. It is designed to be read by an AI agent building a custom connector (Meta Muse, Claude, ChatGPT or similar) without needing to log in.\n\n**How it works**\n1. The account holder creates an API key at https://medshotsai.com/dashboard/settings.\n2. They paste it into your agent's credential store — never into a chat message.\n3. You send 2-4 photos of them, pick a `style`, and poll until the headshots are ready.\n\n**Keep it simple**: send `style` (for example `linkedin` or `resume`) and nothing else. It sets the background, clothing, expression and pose in one value, so you do not need to fetch the catalog first. Individual fields are there if you want them, and override the style.\n\n**What this API deliberately will not do**\n- It will not accept a free-text prompt. You choose from enumerated options and the prompt is composed server-side. This keeps output consistent and means a web page your agent read cannot steer the image.\n- It will not spend money. If the account is out of credits you can request a checkout link to hand to the account holder, but only a human can complete a purchase.\n- It will not generate photos of anyone but the account holder. Team and organization features are not reachable with an API key.\n\n**Photos**: 2-4 photos of the same person from different angles produce a far stronger likeness than a single photo.",
    "contact": {
      "name": "MedshotsAI",
      "url": "https://medshotsai.com/for-agents"
    },
    "termsOfService": "https://medshotsai.com/terms"
  },
  "servers": [
    {
      "url": "https://medshotsai.com/api/v1/agent",
      "description": "Production"
    },
    {
      "url": "https://yanivdanan.medshotsai.com/api/v1/agent",
      "description": "Staging"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Check the key and the account's credit balance"
    },
    {
      "name": "Headshots",
      "description": "Create and retrieve headshots"
    },
    {
      "name": "Billing",
      "description": "Hand a checkout link to the account holder"
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Check the key and see what the account can do",
        "description": "Call this first. Confirms the key works and reports the credit balance, so you can tell the account holder they need to top up before you try to generate anything.",
        "operationId": "getAccount",
        "responses": {
          "200": {
            "description": "Account status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/options": {
      "get": {
        "tags": [
          "Headshots"
        ],
        "summary": "List the style options you may choose from",
        "description": "Only needed if you want more control than `style` gives you. `styles` lists the named looks; the other lists are the individual options they are built from. Fetch this rather than hardcoding ids — new options are added over time.",
        "operationId": "listOptions",
        "responses": {
          "200": {
            "description": "Available options",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OptionsResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/headshots": {
      "post": {
        "tags": [
          "Headshots"
        ],
        "summary": "Start generating headshots",
        "description": "Returns immediately with an id. Generation takes roughly a minute — poll GET /headshots/{id} every 10 seconds rather than holding the request open.\n\nSupply photos as `image_urls` (https only) or `images_base64`, or reuse a previous upload with `seed_id`. The easy path is `style` on its own; individual fields override it.",
        "operationId": "createHeadshot",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateHeadshotRequest"
              },
              "examples": {
                "simplest": {
                  "summary": "The usual case — two photos and a style",
                  "value": {
                    "image_urls": [
                      "https://example.com/selfie-front.jpg",
                      "https://example.com/selfie-angle.jpg"
                    ],
                    "style": "linkedin"
                  }
                },
                "resumeBatch": {
                  "summary": "Three resume shots to choose from",
                  "value": {
                    "image_urls": [
                      "https://example.com/a.jpg",
                      "https://example.com/b.jpg"
                    ],
                    "style": "resume",
                    "num_images": 3
                  }
                },
                "override": {
                  "summary": "A style with one field overridden",
                  "value": {
                    "image_urls": [
                      "https://example.com/a.jpg"
                    ],
                    "style": "corporate",
                    "background": "modern-office"
                  }
                },
                "reuseSeed": {
                  "summary": "Reuse photos already uploaded",
                  "value": {
                    "seed_id": "seed_a1b2c3d4e5f6a7b8",
                    "style": "business-casual"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Generation started",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GenerationStarted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Not enough credits. Request a checkout link and give it to the account holder.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InsufficientCredits"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "Headshots"
        ],
        "summary": "List recent headshots",
        "operationId": "listHeadshots",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recent generations",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GenerationList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/headshots/{id}": {
      "get": {
        "tags": [
          "Headshots"
        ],
        "summary": "Check whether the headshots are ready",
        "description": "Poll this every 10 seconds. When `status` is `completed`, `images` holds the download URLs.",
        "operationId": "getHeadshot",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "gen_a1b2c3d4e5f6a7b8"
          }
        ],
        "responses": {
          "200": {
            "description": "Generation status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Generation"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/checkout-link": {
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "Get a checkout URL for the account holder to open",
        "description": "Creates a Stripe Checkout link and returns it. **This does not charge anything.** Give the URL to the account holder to open in their own browser; only they can complete the purchase. There is no endpoint that lets an agent buy credits on its own.",
        "operationId": "createCheckoutLink",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutLinkRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Checkout link created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutLink"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A MedshotsAI API key, sent as `Authorization: Bearer msai_live_...`. The account holder creates one at https://medshotsai.com/dashboard/settings. Store it in your credential vault — it must never be pasted into a chat message."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, invalid, revoked or expired API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InsufficientScope": {
        "description": "The key lacks the scope this endpoint needs",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ValidationFailed": {
        "description": "A field was missing or invalid. The message names the field and, for style options, lists the valid values.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such resource on this account",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Check the X-RateLimit-Reset header.",
        "headers": {
          "X-RateLimit-Limit": {
            "schema": {
              "type": "integer"
            },
            "description": "Requests allowed per minute"
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable machine-readable code",
            "example": "validation_failed"
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation, safe to show the account holder"
          },
          "details": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "required": [
          "error",
          "message"
        ]
      },
      "Account": {
        "type": "object",
        "properties": {
          "account": {
            "type": "object",
            "properties": {
              "user_id": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "nullable": true
              },
              "authenticated_with": {
                "type": "string",
                "enum": [
                  "api_key",
                  "jwt"
                ]
              },
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "credits": {
            "type": "object",
            "properties": {
              "available": {
                "type": "integer"
              },
              "locked": {
                "type": "integer",
                "description": "Expired subscription credits: visible but not spendable"
              },
              "cost_per_photo": {
                "type": "integer"
              },
              "photos_remaining": {
                "type": "integer"
              },
              "out_of_credits": {
                "type": "boolean"
              },
              "how_to_buy": {
                "type": "string"
              }
            }
          },
          "subscription": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string"
              },
              "is_valid": {
                "type": "boolean"
              },
              "next_billing_date": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "limits": {
            "type": "object",
            "properties": {
              "max_photos_per_request": {
                "type": "integer"
              },
              "max_input_photos": {
                "type": "integer"
              },
              "rate_limit_per_minute": {
                "type": "integer"
              }
            }
          }
        }
      },
      "OptionsResponse": {
        "type": "object",
        "properties": {
          "options": {
            "type": "object",
            "properties": {
              "styles": {
                "type": "array",
                "description": "Named looks. Pass one of these ids as `style` — this is the easy path.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    }
                  }
                }
              },
              "background": {
                "$ref": "#/components/schemas/OptionList"
              },
              "attire": {
                "$ref": "#/components/schemas/OptionList"
              },
              "expression": {
                "$ref": "#/components/schemas/OptionList"
              },
              "accessories": {
                "$ref": "#/components/schemas/OptionList"
              },
              "posture": {
                "$ref": "#/components/schemas/OptionList"
              }
            }
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "OptionList": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "id": {
              "type": "string",
              "description": "Pass this value to POST /headshots"
            },
            "label": {
              "type": "string",
              "description": "Human-friendly name, safe to show the account holder"
            }
          }
        }
      },
      "CreateHeadshotRequest": {
        "type": "object",
        "description": "Supply photos one of three ways: image_urls, images_base64, or seed_id. Then either send `style` on its own, or set individual fields.",
        "properties": {
          "image_urls": {
            "type": "array",
            "maxItems": 5,
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "1-5 https URLs of the same person. Must be publicly reachable JPEG, PNG or WebP, at least 512x512, under 20MB each."
          },
          "image_url": {
            "type": "string",
            "format": "uri",
            "description": "Convenience form for a single photo"
          },
          "images_base64": {
            "type": "array",
            "maxItems": 5,
            "items": {
              "type": "string"
            },
            "description": "1-5 base64-encoded photos. A data: URI prefix is accepted."
          },
          "image_base64": {
            "type": "string",
            "description": "Convenience form for a single photo"
          },
          "seed_id": {
            "type": "string",
            "description": "Reuse photos from a previous request instead of uploading again"
          },
          "style": {
            "type": "string",
            "enum": [
              "linkedin",
              "resume",
              "corporate",
              "business-casual",
              "executive",
              "academic",
              "medical",
              "scrubs"
            ],
            "default": "linkedin",
            "description": "The one field most callers need. A named look that sets background, clothing, expression and pose together: linkedin (clean, safe for any industry), resume (crisp on white), corporate (formal — law, finance, consulting), business-casual (tech and startups), executive (blurred office, leadership bios), academic (warm, faculty and research), medical (white coat), scrubs (clinical staff). Individual fields below override it."
          },
          "background": {
            "type": "string",
            "description": "Option id from GET /options Overrides `style`.",
            "example": "studio-gray"
          },
          "attire": {
            "type": "string",
            "example": "white-coat",
            "description": "Overrides `style`."
          },
          "expression": {
            "type": "string",
            "example": "approachable",
            "description": "Overrides `style`."
          },
          "accessories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "stethoscope"
            ],
            "description": "Overrides `style`."
          },
          "posture": {
            "type": "string",
            "example": "tilt-right",
            "description": "Overrides `style`."
          },
          "crossed_arms": {
            "type": "boolean",
            "default": false
          },
          "num_images": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4,
            "default": 1,
            "description": "Each photo costs one credit"
          },
          "name": {
            "type": "string",
            "description": "Optional label for the upload"
          }
        }
      },
      "GenerationStarted": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "gen_a1b2c3d4e5f6a7b8"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing"
            ]
          },
          "seed_id": {
            "type": "string",
            "description": "Pass this to a later request to reuse the same photos"
          },
          "num_images": {
            "type": "integer"
          },
          "credits_used": {
            "type": "integer"
          },
          "credits_remaining": {
            "type": "integer",
            "nullable": true
          },
          "poll_url": {
            "type": "string",
            "format": "uri"
          },
          "eta_seconds": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Generation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed"
            ]
          },
          "progress": {
            "type": "integer",
            "nullable": true
          },
          "seed_id": {
            "type": "string",
            "nullable": true
          },
          "images": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "Present when status is completed"
          },
          "error": {
            "type": "string",
            "description": "Present when status is failed"
          },
          "retry_after_seconds": {
            "type": "integer",
            "description": "Present while still running"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "GenerationList": {
        "type": "object",
        "properties": {
          "generations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Generation"
            }
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "InsufficientCredits": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "insufficient_credits"
            ]
          },
          "message": {
            "type": "string"
          },
          "credits_needed": {
            "type": "integer"
          },
          "credits_available": {
            "type": "integer"
          },
          "how_to_buy": {
            "type": "string"
          }
        }
      },
      "CheckoutLinkRequest": {
        "type": "object",
        "properties": {
          "purchase_type": {
            "type": "string",
            "enum": [
              "credit_pack",
              "subscription"
            ],
            "default": "credit_pack"
          },
          "tier": {
            "type": "string",
            "enum": [
              "basic",
              "professional"
            ],
            "description": "Only for purchase_type=subscription"
          },
          "interval": {
            "type": "string",
            "enum": [
              "month",
              "year"
            ],
            "description": "Only for purchase_type=subscription"
          }
        }
      },
      "CheckoutLink": {
        "type": "object",
        "properties": {
          "checkout_url": {
            "type": "string",
            "format": "uri",
            "description": "Give this to the account holder to open. An agent cannot complete it."
          },
          "purchase_type": {
            "type": "string"
          },
          "credits": {
            "type": "integer",
            "nullable": true
          },
          "instructions": {
            "type": "string"
          }
        }
      }
    }
  }
}
