{
  "openapi": "3.1.0",
  "info": {
    "title": "postcache API",
    "version": "1.0.0",
    "description": "Public, short-lived media URLs for AI agents that post to Instagram, Threads and Facebook. Files are checked against the target platform's media rules; with convert, fixable files are converted in the background.",
    "contact": {
      "email": "hello@postcache.dev"
    }
  },
  "servers": [
    {
      "url": "https://postcache.dev"
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "paths": {
    "/v1/media": {
      "put": {
        "summary": "Upload a file",
        "operationId": "uploadMedia",
        "parameters": [
          {
            "name": "x-target",
            "in": "header",
            "required": true,
            "description": "Platform the file is for",
            "schema": {
              "$ref": "#/components/schemas/Target"
            }
          },
          {
            "name": "x-filename",
            "in": "header",
            "required": false,
            "description": "File name for the URL; the extension follows the stored format",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ttl",
            "in": "query",
            "required": false,
            "description": "Seconds the URL stays valid",
            "schema": {
              "type": "integer",
              "minimum": 3600,
              "maximum": 604800,
              "default": 86400
            }
          },
          {
            "name": "convert",
            "in": "query",
            "required": false,
            "description": "Fix what fails the platform rules",
            "schema": {
              "type": "string",
              "enum": [
                "crop",
                "pad"
              ]
            }
          },
          {
            "name": "dry_run",
            "in": "query",
            "required": false,
            "description": "Check without storing",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "dry_run: checks only, nothing stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DryRun"
                }
              }
            }
          },
          "201": {
            "description": "Stored; url is ready for the platform API",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Media"
                }
              }
            }
          },
          "202": {
            "description": "Queued for conversion; poll GET /v1/media/{id} until status is ready or failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Queued"
                }
              }
            }
          },
          "400": {
            "description": "Bad target, ttl or convert, or no body (error: bad_target | bad_ttl | bad_convert | empty)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "File over 1 GiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Fails the platform rules and conversion was not requested or cannot fix it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChecksFailed"
                }
              }
            }
          },
          "503": {
            "description": "Too many uploads in progress",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Plan limit reached (error: quota_exceeded, upgrade_url, buy_url), or no key: pay per use (error: payment_required; challenges in WWW-Authenticate (MPP) and Payment-Required (x402), 0.01 USD)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Key's daily cap reached (error: budget_exceeded); Retry-After until 00:00 UTC",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Payment already used (error: payment_reused)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          },
          {}
        ]
      }
    },
    "/v1/media/from-url": {
      "post": {
        "summary": "Upload a file from a public URL",
        "operationId": "uploadMediaFromUrl",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "source_url",
                  "target"
                ],
                "properties": {
                  "source_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Public http(s) URL; private and internal addresses are refused"
                  },
                  "target": {
                    "$ref": "#/components/schemas/Target"
                  },
                  "filename": {
                    "type": "string"
                  },
                  "ttl": {
                    "type": "integer",
                    "minimum": 3600,
                    "maximum": 604800
                  },
                  "convert": {
                    "type": "string",
                    "enum": [
                      "crop",
                      "pad"
                    ]
                  },
                  "dry_run": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "dry_run: checks only, nothing stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DryRun"
                }
              }
            }
          },
          "201": {
            "description": "Stored; url is ready for the platform API",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Media"
                }
              }
            }
          },
          "202": {
            "description": "Queued for conversion; poll GET /v1/media/{id} until status is ready or failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Queued"
                }
              }
            }
          },
          "400": {
            "description": "Bad target, ttl, convert or source (error: bad_target | bad_ttl | bad_convert | bad_url | blocked | fetch_failed)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "File over 1 GiB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Fails the platform rules and conversion was not requested or cannot fix it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChecksFailed"
                }
              }
            }
          },
          "503": {
            "description": "Too many uploads in progress",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Plan limit reached (error: quota_exceeded, upgrade_url, buy_url), or no key: pay per use (error: payment_required; challenges in WWW-Authenticate (MPP) and Payment-Required (x402), 0.01 USD)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Key's daily cap reached (error: budget_exceeded); Retry-After until 00:00 UTC",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Payment already used (error: payment_reused)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          },
          {}
        ]
      }
    },
    "/v1/media/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Media id",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "summary": "Status of an upload",
        "operationId": "getMedia",
        "responses": {
          "200": {
            "description": "Status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MediaStatus"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found (or not yours)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "x-delete-token",
            "in": "header",
            "required": false,
            "description": "Pay-per-use uploads: the delete_token from the upload answer, instead of a key",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearer": []
          },
          {}
        ]
      },
      "delete": {
        "summary": "Delete an upload now",
        "operationId": "deleteMedia",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found (or not yours)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "x-delete-token",
            "in": "header",
            "required": false,
            "description": "Pay-per-use uploads: the delete_token from the upload answer, instead of a key",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearer": []
          },
          {}
        ]
      }
    },
    "/m/{id}/{name}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Media id",
          "schema": {
            "type": "string"
          }
        },
        {
          "name": "name",
          "in": "path",
          "required": true,
          "description": "File name from the upload answer",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "summary": "The public file (media host)",
        "operationId": "getFile",
        "security": [],
        "servers": [
          {
            "url": "https://cdn.postcache.dev"
          }
        ],
        "description": "Served only on cdn.postcache.dev; no key needed. Supports single byte ranges (Range: bytes=start-end).",
        "parameters": [
          {
            "name": "Range",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file"
          },
          "206": {
            "description": "Partial content"
          },
          "404": {
            "description": "Unknown id or name"
          },
          "410": {
            "description": "Expired or deleted"
          },
          "416": {
            "description": "Range starts past the end (Content-Range: bytes */size)"
          }
        }
      }
    },
    "/v1/credits": {
      "post": {
        "summary": "Buy a credit pack (500 uploads for 5 USD)",
        "operationId": "buyCredits",
        "security": [
          {
            "bearer": []
          },
          {}
        ],
        "description": "Pay with MPP (card or USDC on Tempo) or x402 (USDC on Base). Without a key, creates an account with 500 credits and returns its API key once; with a key, adds 500 credits to that account.",
        "responses": {
          "200": {
            "description": "Credits added to the calling account"
          },
          "201": {
            "description": "New account: { key, credits, payment }"
          },
          "402": {
            "description": "Payment challenge (error: payment_required)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Payment already used",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "pc_live_... early-access key"
      }
    },
    "schemas": {
      "Target": {
        "type": "string",
        "enum": [
          "instagram.feed",
          "instagram.carousel",
          "instagram.reels",
          "instagram.story",
          "threads",
          "facebook.page"
        ]
      },
      "Problem": {
        "type": "object",
        "properties": {
          "rule": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "fix": {
            "type": "string"
          }
        }
      },
      "Checks": {
        "type": "object",
        "properties": {
          "target": {
            "$ref": "#/components/schemas/Target"
          },
          "pass": {
            "type": "boolean"
          },
          "kind": {
            "type": "string",
            "enum": [
              "image",
              "video",
              "unknown",
              "unreadable"
            ]
          },
          "problems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Problem"
            }
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Media": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "ready"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "target": {
            "$ref": "#/components/schemas/Target"
          },
          "content_type": {
            "type": "string"
          },
          "size": {
            "type": "integer"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "checks": {
            "$ref": "#/components/schemas/Checks"
          }
        }
      },
      "Queued": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "converting"
            ]
          },
          "target": {
            "$ref": "#/components/schemas/Target"
          },
          "original_checks": {
            "$ref": "#/components/schemas/Checks"
          },
          "status_url": {
            "type": "string"
          }
        }
      },
      "MediaStatus": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "converting",
              "ready",
              "failed",
              "deleted"
            ]
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set only when status is ready"
          },
          "target": {
            "$ref": "#/components/schemas/Target"
          },
          "content_type": {
            "type": "string"
          },
          "size": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "deleted": {
            "type": "boolean"
          },
          "fetch_count": {
            "type": "integer"
          },
          "checks": {
            "$ref": "#/components/schemas/Checks"
          },
          "converted": {
            "type": "boolean"
          },
          "original_checks": {
            "$ref": "#/components/schemas/Checks"
          },
          "failure_reason": {
            "type": "string",
            "description": "Why a conversion failed"
          }
        }
      },
      "DryRun": {
        "type": "object",
        "properties": {
          "dry_run": {
            "type": "boolean"
          },
          "checks": {
            "$ref": "#/components/schemas/Checks"
          },
          "convertible": {
            "type": "boolean",
            "description": "Present when convert was given"
          }
        }
      },
      "ChecksFailed": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "checks_failed"
            ]
          },
          "checks": {
            "$ref": "#/components/schemas/Checks"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "upgrade_url": {
            "type": "string",
            "description": "Where to change the plan (with quota_exceeded)"
          }
        }
      }
    }
  }
}
