{
  "openapi": "3.1.0",
  "info": {
    "title": "PiPic Compression API",
    "version": "1.0.0",
    "description": "HTTP contract for the image-compression engine behind pipic.cc and the @pipic/cli Agent CLI. One endpoint: send raw image bytes, get the compressed image back. The same contract, written for humans, is documented at https://pipic.cc/cli#api.",
    "contact": {
      "name": "PiPic",
      "email": "contact@pipic.cc",
      "url": "https://pipic.cc/contact"
    }
  },
  "servers": [
    {
      "url": "https://pipic.cc",
      "description": "Production — the only environment this spec describes."
    }
  ],
  "paths": {
    "/compress/image": {
      "post": {
        "operationId": "compressImage",
        "summary": "Compress an image",
        "description": "Compresses a single PNG, JPEG, WebP or AVIF image and streams the result back — same format in, same format out. One image per request; for batches, send multiple requests (the Agent CLI defaults to 4 concurrent uploads).",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "Content-Type",
            "in": "header",
            "required": true,
            "description": "MIME type of the bytes in the request body. Must be one of the four supported types and must match what you actually send — it is not sniffed from the bytes.",
            "schema": {
              "type": "string",
              "enum": [
                "image/png",
                "image/jpeg",
                "image/webp",
                "image/avif"
              ]
            }
          },
          {
            "name": "X-Original-Name",
            "in": "header",
            "required": false,
            "description": "Optional filename, URL-encoded with encodeURIComponent. Echoed back under the same header on the 200 response. A value containing characters outside the encodeURIComponent output set is silently dropped from the response — it never fails the request.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Content-Length",
            "in": "header",
            "required": false,
            "description": "Standard HTTP header. When present and greater than 8388608 bytes, the request is rejected with 413 before the body is read — rejected before the quota counter runs — as are any 401 and a 429 RATE_LIMITED, both of which are handled by middleware ahead of it.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Raw image bytes — not multipart/form-data, no wrapper field. Up to 8388608 bytes (8 MB).",
          "content": {
            "image/png": {
              "schema": {
                "contentMediaType": "image/png"
              }
            },
            "image/jpeg": {
              "schema": {
                "contentMediaType": "image/jpeg"
              }
            },
            "image/webp": {
              "schema": {
                "contentMediaType": "image/webp"
              }
            },
            "image/avif": {
              "schema": {
                "contentMediaType": "image/avif"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Compression succeeded. The response body is the compressed image — same format in, same format out.",
            "headers": {
              "Content-Type": {
                "description": "Mirrors the compressed image format.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Length": {
                "description": "Present when known ahead of streaming the body.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Original-Name": {
                "description": "Echoed back only if you sent one and it survived URL-encoding validation.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "image/png": {
                "schema": {
                  "contentMediaType": "image/png"
                }
              },
              "image/jpeg": {
                "schema": {
                  "contentMediaType": "image/jpeg"
                }
              },
              "image/webp": {
                "schema": {
                  "contentMediaType": "image/webp"
                }
              },
              "image/avif": {
                "schema": {
                  "contentMediaType": "image/avif"
                }
              }
            }
          },
          "400": {
            "description": "The request is malformed. This still counts as one attempt against your quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "INVALID_TYPE": {
                    "value": {
                      "success": false,
                      "code": "INVALID_TYPE",
                      "message": "Content-Type must be an image type"
                    }
                  },
                  "NO_FILE": {
                    "value": {
                      "success": false,
                      "code": "NO_FILE",
                      "message": "Request body is empty"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "UNAUTHORIZED": {
                    "value": {
                      "success": false,
                      "code": "UNAUTHORIZED",
                      "message": "Unauthorized"
                    }
                  },
                  "TOKEN_EXPIRED": {
                    "value": {
                      "success": false,
                      "code": "TOKEN_EXPIRED",
                      "message": "Token has expired, sign in again to get a new one"
                    }
                  },
                  "TOKEN_REVOKED": {
                    "value": {
                      "success": false,
                      "code": "TOKEN_REVOKED",
                      "message": "Token has been revoked, create a new one in your account"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The file is larger than 8 MB. Checked twice: once against a declared Content-Length before the body is read (free), once against the actual byte count after (costs one quota attempt, catches chunked uploads with no declared length).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "FILE_TOO_LARGE": {
                    "value": {
                      "success": false,
                      "code": "FILE_TOO_LARGE",
                      "message": "File exceeds 8MB limit"
                    }
                  }
                }
              }
            }
          },
          "415": {
            "description": "Content-Type was a valid image/* type, but not one this API compresses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "UNSUPPORTED_TYPE": {
                    "value": {
                      "success": false,
                      "code": "UNSUPPORTED_TYPE",
                      "message": "Only PNG, JPEG, WebP and AVIF are supported"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Either the per-IP rate limit (100 requests / 60s) or your monthly quota (Free: 100 images/month, Pro: 5,000/month — resets the 1st of the month, UTC, no rollover) was hit. RATE_LIMITED is transient and comes with a `Retry-After` header; MONTHLY_QUOTA_EXCEEDED is not — retrying before next month fails the same way every time.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. Present only on RATE_LIMITED, never on MONTHLY_QUOTA_EXCEEDED.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "RATE_LIMITED": {
                    "value": {
                      "success": false,
                      "code": "RATE_LIMITED",
                      "message": "Too many requests, please slow down"
                    }
                  },
                  "MONTHLY_QUOTA_EXCEEDED": {
                    "value": {
                      "success": false,
                      "code": "MONTHLY_QUOTA_EXCEEDED",
                      "message": "Monthly compression quota reached, try again next month"
                    }
                  }
                }
              }
            }
          },
          "502": {
            "description": "A non-retryable failure on our compression backend — the same bytes will fail the same way. If you authenticated with a token, this attempt is refunded to your monthly quota automatically.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "UPSTREAM_ERROR": {
                    "value": {
                      "success": false,
                      "code": "UPSTREAM_ERROR",
                      "message": "Compression service error"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Our compression backend is temporarily out of capacity or failing upstream. Safe to retry with backoff. If you authenticated with a token, this attempt's quota is refunded automatically — it does not count against you.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "UPSTREAM_ERROR": {
                    "value": {
                      "success": false,
                      "code": "UPSTREAM_ERROR",
                      "message": "Compression service temporarily unavailable"
                    }
                  },
                  "ALL_KEYS_EXHAUSTED": {
                    "value": {
                      "success": false,
                      "code": "ALL_KEYS_EXHAUSTED",
                      "message": "Compression capacity temporarily exhausted, please try again later"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "pipic_<random>",
        "description": "A Personal Access Token, created at https://pipic.cc/account after signing in with GitHub or Google. This is the same token type the Agent CLI stores locally after `pipic login`."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "success",
          "code",
          "message"
        ],
        "properties": {
          "success": {
            "const": false
          },
          "code": {
            "type": "string",
            "description": "Machine-readable error code. Branch your code on this field, never on `message` — the message text is not a stable contract."
          },
          "message": {
            "type": "string",
            "description": "Human-readable, in English. Safe to log; not localized, not meant for end users."
          }
        }
      }
    }
  }
}