{
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable; branch on this",
                "example": "missing_scope"
              },
              "message": {
                "type": "string",
                "description": "A sentence for a person; do not parse"
              }
            }
          }
        }
      }
    },
    "securitySchemes": {
      "accessToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "tka_",
        "description": "An access token from POST /v1/token. It lives 15 minutes; ask for another when it expires. The client ID and secret are not accepted here — only at /v1/token."
      },
      "clientCredentials": {
        "type": "http",
        "scheme": "basic",
        "description": "Username: the client ID. Password: the client secret. Accepted only at POST /v1/token."
      }
    }
  },
  "info": {
    "title": "Tekomata API",
    "version": "1",
    "description": "The Tekomata API. Today it does one thing: it writes a blog post with AI, paid for from the shop's Tekomata credit.\n\n**Authentication.** The shop creates an integration in *Settings → API integrations*, ticks **Write a blog post with AI**, and gives you a **client ID** (`tkc_…`) and a **client secret** (`tk_live_…`, shown once). Trade them for an access token:\n\n`curl -s -u CLIENT_ID:CLIENT_SECRET -X POST https://api.tekomata.com/v1/token`\n\nSend the token as `Authorization: Bearer tka_…`. It lasts **15 minutes**; ask for another when it expires. There is no refresh token — your client secret is all you need to get a new one. The client ID and secret are accepted at `/v1/token` and nowhere else. Click **Authorize** above to try the call here.\n\n**What it costs.** Every generated post is charged to the shop's prepaid credit: the AI provider's tokens at cost, plus the plan's fixed base. The reply tells you what it took, in `charged`. A call that fails costs nothing. Token cost is capped at Rp 2.000 for one post; anything past that is ours, not the shop's.\n\n**Money** is whole rupiah in `charged`, and a decimal string (`\"4400.0000\"`) everywhere else, never a float. **Times** are RFC 3339.\n\n**Errors** always look like `{\"error\": {\"code\": \"...\", \"message\": \"...\"}}`. Branch on `code`.\n\n**Rate limit.** Per integration, default 60 a minute. `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and on 429 `Retry-After`.\n\nEverything your integration does is logged and visible to the shop: every call, and every write in its audit trail.\n\n**More is coming.** If you need something this API does not do yet, ask the shop to tell us."
  },
  "openapi": "3.0.3",
  "paths": {
    "/v1/blog/generate": {
      "post": {
        "tags": [
          "Blog generator"
        ],
        "summary": "Write a blog post with AI · blog:generate",
        "description": "Sends the prompt to a model and returns the post as blocks. Paid from the shop's prepaid credit: the call is made with Tekomata's provider key and the cost — the provider's tokens at cost, plus the plan's fixed base — is taken off the shop's balance and returned in `charged`.\n\nThe balance is checked before the call, so a shop without credit is refused with `402` rather than billed afterwards. A failed call costs nothing. Token cost is capped at Rp 2.000 for one post; anything beyond that is ours, not the shop's.",
        "operationId": "generateBlogPost",
        "x-required-scope": "blog:generate",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "prompt"
                ],
                "properties": {
                  "prompt": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "What the post should be about. Write it in the language you want the post in.",
                    "example": "Manfaat botol PET untuk usaha minuman kecil"
                  },
                  "provider": {
                    "type": "string",
                    "description": "Optional. Empty means no preference.",
                    "example": "anthropic"
                  },
                  "model": {
                    "type": "string",
                    "description": "Optional, and only valid with provider.",
                    "example": "claude-opus-5"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The post, and what it cost.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "blocks",
                    "provider",
                    "model",
                    "charged"
                  ],
                  "properties": {
                    "blocks": {
                      "type": "array",
                      "description": "In order. Exactly one block has level h1: it is the title, and it comes first.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "level": {
                            "type": "string",
                            "enum": [
                              "h1",
                              "h2",
                              "h3",
                              "p"
                            ]
                          },
                          "content": {
                            "type": "string",
                            "description": "Plain text. Never HTML or markdown."
                          }
                        }
                      }
                    },
                    "adjustments": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "What the parser had to correct in the model's reply. Usually empty."
                    },
                    "provider": {
                      "type": "string",
                      "example": "anthropic"
                    },
                    "model": {
                      "type": "string",
                      "example": "claude-opus-5"
                    },
                    "charged": {
                      "type": "object",
                      "description": "What this post took off the shop's balance, in whole rupiah.",
                      "properties": {
                        "currency": {
                          "type": "string",
                          "example": "IDR"
                        },
                        "tokens": {
                          "type": "integer",
                          "example": 75,
                          "description": "The provider's cost, at cost. Capped at 2000."
                        },
                        "base": {
                          "type": "integer",
                          "example": 1000,
                          "description": "The plan's fixed price per reply."
                        },
                        "total": {
                          "type": "integer",
                          "example": 1075
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Not enough credit. Nothing was called and nothing was charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The prompt is empty or longer than 2000 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The model answered, but not with a usable post. The shop was charged, because the call happened.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "No AI model is available on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/blog/models": {
      "get": {
        "tags": [
          "Blog generator"
        ],
        "summary": "Which models can write · blog:generate",
        "description": "The values `provider` and `model` may be set to on a generate call. **The first entry is the default**: send neither field and that is what writes the post.\n\nRead it rather than hard-coding a pair. A model can be switched off or repriced, and a hard-coded one would start failing with no change on your side.",
        "operationId": "listBlogModels",
        "x-required-scope": "blog:generate",
        "responses": {
          "200": {
            "description": "The models on offer, in preference order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "models"
                  ],
                  "properties": {
                    "models": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "provider": {
                            "type": "string",
                            "example": "anthropic"
                          },
                          "model": {
                            "type": "string",
                            "example": "claude-opus-5",
                            "description": "Pass this and provider together, or neither."
                          },
                          "label": {
                            "type": "string",
                            "example": "Claude Opus 5",
                            "description": "For a dropdown. The model id means nothing to a shopkeeper."
                          },
                          "isDefault": {
                            "type": "boolean",
                            "description": "True for exactly one entry, the first."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "No AI model is available on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "security": [
    {
      "accessToken": []
    }
  ],
  "servers": [
    {
      "url": "https://api.tekomata.com"
    }
  ],
  "tags": [
    {
      "description": "Write a blog post with AI, paid from the shop's Tekomata credit.",
      "name": "Blog generator"
    }
  ]
}