{
  "openapi": "3.1.0",
  "info": {
    "title": "Rendry API",
    "version": "1.4.0",
    "description": "Templated image generation API. Design a template in the visual editor (or via POST /presets), then render on-brand images with one call. For AI agents: a remote MCP endpoint is available at /mcp (streamable HTTP) — see https://rendry.io/llms-full.txt",
    "contact": {
      "url": "https://rendry.io"
    }
  },
  "servers": [
    {
      "url": "https://rendry.io/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key sk_live_... — create at Settings → API Keys"
      }
    },
    "schemas": {
      "RenderRequest": {
        "type": "object",
        "required": [
          "preset_id"
        ],
        "properties": {
          "preset_id": {
            "type": "string",
            "description": "Template ID from GET /presets"
          },
          "dynamic_data": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Variable substitutions: text variables take text, dynamic image slots take a public image URL"
          },
          "webhook_url": {
            "type": "string",
            "description": "Async only: POST-ed {job_id,status,result_url} on completion"
          }
        }
      },
      "SyncRenderResponse": {
        "type": "object",
        "properties": {
          "job_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "success",
              "error"
            ]
          },
          "result_url": {
            "type": "string",
            "description": "URL of the rendered image"
          },
          "error": {
            "type": "string"
          }
        }
      },
      "AsyncRenderResponse": {
        "type": "object",
        "properties": {
          "job_id": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "status_url": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "RenderJobStatus": {
        "type": "object",
        "properties": {
          "job_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "processing",
              "done",
              "error"
            ]
          },
          "result_url": {
            "type": "string"
          },
          "error": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          },
          "started_at": {
            "type": "string"
          },
          "completed_at": {
            "type": "string"
          }
        }
      },
      "PresetOverview": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "preview_url": {
            "type": "string"
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          }
        }
      },
      "TemplateIndex": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "canvas_size": {
            "type": "string",
            "example": "1080x1080"
          },
          "preview_url": {
            "type": "string"
          },
          "variables": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Variable names the template accepts via dynamic_data"
          },
          "sample_data": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Realistic example values for every variable"
          }
        }
      },
      "UserLimits": {
        "type": "object",
        "properties": {
          "plan": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "presets": {
            "type": "object",
            "properties": {
              "used": {
                "type": "integer"
              },
              "limit": {
                "type": "integer"
              }
            }
          },
          "assets": {
            "type": "object",
            "properties": {
              "used": {
                "type": "integer"
              },
              "limit": {
                "type": "integer"
              }
            }
          },
          "renders_this_month": {
            "type": "object",
            "properties": {
              "used": {
                "type": "integer"
              },
              "limit": {
                "type": "integer"
              }
            }
          },
          "resets_at": {
            "type": "string",
            "format": "date-time"
          },
          "upgrade_url": {
            "type": "string"
          }
        }
      }
    }
  },
  "paths": {
    "/render/sync": {
      "post": {
        "summary": "Render an image synchronously",
        "description": "Renders the template with dynamic_data substituted. Waits up to 25s (typical: 1-3s). Returns result_url of the finished PNG. Body flag \"preview\": true renders without counting against the API render quota (design iteration).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rendered",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SyncRenderResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (error text names the field)"
          },
          "401": {
            "description": "Missing/invalid API key"
          },
          "404": {
            "description": "Preset not found"
          },
          "408": {
            "description": "Render timeout"
          },
          "422": {
            "description": "Render failed (error text explains why)"
          },
          "429": {
            "description": "Monthly render quota exceeded"
          }
        }
      }
    },
    "/render/async": {
      "post": {
        "summary": "Render an image asynchronously",
        "description": "Enqueues the render and returns 202 immediately. Poll /render/status/{job_id} or pass webhook_url.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenderRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AsyncRenderResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid API key"
          },
          "429": {
            "description": "Monthly render quota exceeded"
          }
        }
      }
    },
    "/render/status/{job_id}": {
      "get": {
        "summary": "Get render job status",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RenderJobStatus"
                }
              }
            }
          },
          "404": {
            "description": "Job not found"
          }
        }
      }
    },
    "/presets": {
      "get": {
        "summary": "List your templates",
        "responses": {
          "200": {
            "description": "Templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PresetOverview"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a template from JSON DSL",
        "description": "Full DSL reference: https://rendry.io/docs. Canvas up to 4096x4096, up to 20 layers.",
        "responses": {
          "201": {
            "description": "Created"
          },
          "400": {
            "description": "Validation error"
          }
        }
      }
    },
    "/presets/{id}": {
      "get": {
        "summary": "Get full template (canvas + layers)",
        "description": "Layers reveal {variable} placeholders and dynamic image keys — everything render_image accepts in dynamic_data.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Template"
          },
          "404": {
            "description": "Not found (or owned by another account)"
          }
        }
      }
    },
    "/capabilities": {
      "get": {
        "summary": "Service capabilities",
        "description": "Layer types, available fonts, canvas limits, max layers.",
        "security": [],
        "responses": {
          "200": {
            "description": "Capabilities"
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "summary": "Remote MCP endpoint (streamable HTTP)",
        "description": "Model Context Protocol over stateless HTTP: POST JSON-RPC 2.0 messages (initialize, tools/list, tools/call). 17 tools: gallery (list_templates, preview_template, create_preset_from_template), design CRUD from raw JSON DSL with strict field validation (get_preset, create_preset, update_preset with layer_patches, delete_preset), preview_preset (free renders for iteration), get_capabilities, render_image (returns the image inline), list_presets, get_render_status, get_usage. Zero-to-image flow for agents: list_templates → create_preset_from_template → render_image. Agent guide: https://rendry.io/llms-full.txt",
        "responses": {
          "200": {
            "description": "JSON-RPC response"
          },
          "401": {
            "description": "Missing/invalid API key"
          }
        }
      }
    },
    "/templates": {
      "get": {
        "summary": "Public template gallery",
        "description": "39 ready-made designs (social posts, YouTube thumbnails, sale banners, OG images, posters, certificates, labels) with variables and sample data. No authentication required.",
        "security": [],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter: marketing, social, og, documents, events, product"
          }
        ],
        "responses": {
          "200": {
            "description": "Templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/TemplateIndex"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/templates/{id}": {
      "get": {
        "summary": "Full template JSON (canvas + layers)",
        "description": "The returned document is a valid body for POST /presets — clone a gallery template into the account by piping one into the other.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Template DSL"
          },
          "404": {
            "description": "Template not found"
          }
        }
      }
    },
    "/user/limits": {
      "get": {
        "summary": "Plan and quota usage",
        "description": "Current plan, renders/presets/assets used vs limits, quota reset date and an upgrade URL. Works with an API key — agents use this to check remaining quota.",
        "responses": {
          "200": {
            "description": "Usage",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserLimits"
                }
              }
            }
          },
          "401": {
            "description": "Missing/invalid credentials"
          }
        }
      }
    },
    "/presets/{id}/group": {
      "delete": {
        "summary": "Delete a preset (all versions) and free a template slot",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted"
          },
          "401": {
            "description": "Missing/invalid credentials"
          }
        }
      }
    },
    "/assets/import": {
      "post": {
        "summary": "Import an asset (image or TTF/OTF font) from base64 or a public URL",
        "description": "JSON alternative to multipart upload, usable with an API key. Body: {filename, data_base64|url}. Images ≤5MB (inline base64 is additionally bounded by the 2MB request body; for larger files use url, or POST multipart to /assets). Fonts TTF/OTF. Returns the stored asset with its public url. Note: MCP tool calls apply a stricter 96KB cap on data_base64 — inline base64 there costs the calling model output tokens.",
        "responses": {
          "201": {
            "description": "Asset created"
          },
          "400": {
            "description": "Invalid payload or unsupported type"
          },
          "403": {
            "description": "Asset quota exceeded"
          }
        }
      }
    }
  }
}