{
  "openapi": "3.1.0",
  "info": {
    "title": "Friends API",
    "version": "1.0",
    "description": "Characters, episodes and quotes from the sitcom universe. Practice CRUD, Bearer auth, pagination, filtering and every error code — no setup needed."
  },
  "servers": [
    {
      "url": "https://funapi.dev/api/friends/v1"
    }
  ],
  "tags": [
    {
      "name": "characters",
      "description": "the six of them, and more"
    },
    {
      "name": "episodes",
      "description": "ten fan favorites"
    },
    {
      "name": "quotes",
      "description": "could this API BE any more quotable?"
    },
    {
      "name": "graphql",
      "description": "one POST endpoint, query-shaped"
    }
  ],
  "paths": {
    "/characters": {
      "get": {
        "tags": [
          "characters"
        ],
        "summary": "List all characters (paginated) — v1, deprecated",
        "operationId": "fr-list",
        "responses": {
          "200": {
            "description": "Paginated character list, with Deprecation/Sunset/Link headers pointing at v2",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "Invalid page parameter, or an unknown query parameter (strict mode)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Page out of range",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, default 1. Out of range → 404, non-integer → 400.",
            "schema": {
              "type": "integer",
              "format": "int32"
            },
            "example": 1
          },
          {
            "name": "job",
            "in": "query",
            "required": false,
            "description": "Filter by occupation, e.g. chef, actor, barista.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "deprecated": true
      },
      "post": {
        "tags": [
          "characters"
        ],
        "summary": "Create a character",
        "operationId": "fr-post",
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            },
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri-reference"
                }
              }
            }
          },
          "400": {
            "description": "Missing name / invalid JSON",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Repeat the same key on retry to get the original response instead of a duplicate.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "job": {
                    "type": "string"
                  },
                  "catchphrase": {
                    "type": "string"
                  }
                },
                "required": [
                  "name",
                  "job",
                  "catchphrase"
                ],
                "additionalProperties": false
              },
              "example": {
                "name": "Gunther Jr.",
                "job": "barista",
                "catchphrase": "More coffee?"
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/characters/{id}": {
      "get": {
        "tags": [
          "characters"
        ],
        "summary": "Get one character",
        "operationId": "fr-get",
        "responses": {
          "200": {
            "description": "The character",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "Invalid id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Character id. Try 999 for a 404.",
            "schema": {
              "type": "integer",
              "format": "int32"
            },
            "example": 1
          }
        ]
      },
      "put": {
        "tags": [
          "characters"
        ],
        "summary": "Update a character",
        "operationId": "fr-put",
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "Empty body / invalid JSON",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "If-Match does not match current version",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Character id to update.",
            "schema": {
              "type": "integer",
              "format": "int32"
            },
            "example": 7
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": false,
            "description": "Optimistic concurrency: pass a prior etag (e.g. v1). Mismatch → 412.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "job": {
                    "type": "string"
                  }
                },
                "required": [
                  "job"
                ],
                "additionalProperties": false
              },
              "example": {
                "job": "coffee shop owner"
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      },
      "delete": {
        "tags": [
          "characters"
        ],
        "summary": "Delete a character",
        "operationId": "fr-del",
        "responses": {
          "204": {
            "description": "Deleted, empty body"
          },
          "401": {
            "description": "Missing or invalid token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Viewer token not allowed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "412": {
            "description": "If-Match does not match current version",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Character id. Viewer token → 403.",
            "schema": {
              "type": "integer",
              "format": "int32"
            },
            "example": 12
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": false,
            "description": "Optimistic concurrency check. Mismatch → 412.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/characters/bulk": {
      "post": {
        "tags": [
          "characters"
        ],
        "summary": "Batch-create characters (array body, per-item validation)",
        "operationId": "fr-bulk",
        "responses": {
          "201": {
            "description": "All items created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            },
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri-reference"
                }
              }
            }
          },
          "207": {
            "description": "Partial success — some items failed validation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "Body is not a JSON array",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "job": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "name",
                    "job"
                  ],
                  "additionalProperties": false
                }
              },
              "example": [
                {
                  "name": "Gunther Jr.",
                  "job": "barista"
                },
                {
                  "name": "Estelle II",
                  "job": "agent"
                }
              ]
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/characters/bulk-delete": {
      "post": {
        "tags": [
          "characters"
        ],
        "summary": "Bulk-delete characters by id (partial success supported)",
        "operationId": "fr-bulk-del",
        "responses": {
          "200": {
            "description": "All ids existed and were deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "207": {
            "description": "Partial success — some ids did not exist",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "\"ids\" is missing, empty, or not an array of integers",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Viewer token not allowed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  }
                },
                "required": [
                  "ids"
                ],
                "additionalProperties": false
              },
              "example": {
                "ids": [
                  11,
                  12,
                  999
                ]
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/episodes": {
      "get": {
        "tags": [
          "episodes"
        ],
        "summary": "List episodes, filter by season",
        "operationId": "fr-eps",
        "responses": {
          "200": {
            "description": "Episode list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "Non-integer season",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "season",
            "in": "query",
            "required": false,
            "description": "Season 1–10. Try \"five\" for a 400.",
            "schema": {
              "type": "integer",
              "format": "int32"
            },
            "example": 5
          }
        ]
      }
    },
    "/quotes/random": {
      "get": {
        "tags": [
          "quotes"
        ],
        "summary": "A random quote",
        "operationId": "fr-quote",
        "responses": {
          "200": {
            "description": "One random quote",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      }
    },
    "/characters/{id}/quotes": {
      "get": {
        "tags": [
          "characters"
        ],
        "summary": "Get a character's quotes (nested resource)",
        "operationId": "fr-char-quotes",
        "responses": {
          "200": {
            "description": "Matching quotes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "404": {
            "description": "Unknown character",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Character id.",
            "schema": {
              "type": "integer",
              "format": "int32"
            },
            "example": 2
          }
        ]
      }
    },
    "/friends/v2/characters": {
      "get": {
        "tags": [
          "characters"
        ],
        "summary": "List characters — v2 (breaking change: job → occupation, re-wrapped)",
        "operationId": "fr-v2-list",
        "responses": {
          "200": {
            "description": "v2 shape: { apiVersion, data: { items, count } }, occupation replaces job",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "Invalid page parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Page out of range",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Page number, default 1.",
            "schema": {
              "type": "integer",
              "format": "int32"
            },
            "example": 1
          }
        ],
        "servers": [
          {
            "url": "https://funapi.dev/api"
          }
        ]
      }
    },
    "/friends/version": {
      "get": {
        "tags": [
          "characters"
        ],
        "summary": "Discover supported API versions",
        "operationId": "fr-version",
        "responses": {
          "200": {
            "description": "Current, supported and deprecated versions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        },
        "servers": [
          {
            "url": "https://funapi.dev/api"
          }
        ]
      }
    },
    "/friends/characters": {
      "get": {
        "tags": [
          "characters"
        ],
        "summary": "Content-negotiated version via Accept header (406 if unsupported)",
        "operationId": "fr-negotiate",
        "responses": {
          "200": {
            "description": "Shape depends on negotiated version",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "406": {
            "description": "Accept header requests an unsupported media type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Accept",
            "in": "header",
            "required": false,
            "description": "application/vnd.friends.v1+json or application/vnd.friends.v2+json (default: v2). Anything else → 406.",
            "schema": {
              "type": "string"
            },
            "example": "application/vnd.friends.v2+json"
          }
        ],
        "servers": [
          {
            "url": "https://funapi.dev/api"
          }
        ]
      }
    },
    "/graphql": {
      "post": {
        "tags": [
          "graphql"
        ],
        "summary": "Run a query or mutation over character data",
        "operationId": "gq-post",
        "responses": {
          "200": {
            "description": "GraphQL convention: always 200, with data and/or an errors array",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "Missing \"query\" string, or completely unrecognized syntax",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "query": {
                    "type": "string"
                  }
                },
                "required": [
                  "query"
                ],
                "additionalProperties": false
              },
              "example": {
                "query": "{ characters { id name job } }"
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Use qa-admin-token / qa-viewer-token, or a JWT from POST /auth/v1/login."
      },
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Cargo demo key: cargo-key-123."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "requestId": {
            "type": "string"
          }
        },
        "additionalProperties": true
      }
    }
  }
}
