{
  "openapi": "3.1.0",
  "info": {
    "title": "KcalSnap agent nutrition API",
    "version": "1.0.0",
    "description": "Public beta. Estimates calories and macros from food photos. 0.01 USDC per successful analysis via x402 v2 exact on Base. No account required. Only purchase within an owner-authorized budget. Card-funded prepaid access is also available: $5 USD for 500 successful scans. Cardholder completes Stripe Checkout, then REST and MCP use a Bearer API key.",
    "contact": {
      "email": "hello@kcalsnap.com"
    }
  },
  "servers": [
    {
      "url": "https://kcalsnap.com/api"
    }
  ],
  "externalDocs": {
    "url": "https://kcalsnap.com/developers/"
  },
  "paths": {
    "/health": {
      "get": {
        "operationId": "health",
        "responses": {
          "200": {
            "description": "Service version and payment network"
          }
        }
      }
    },
    "/v1/analyze": {
      "post": {
        "operationId": "analyzeFood",
        "summary": "Estimate nutrition using one prepaid credit or 0.01 USDC",
        "description": "An unpaid request returns PaymentRequired. Retry with PAYMENT-SIGNATURE. Persist the exact image, idempotency key and signed authorization for recovery. Do not create another payment after payment_pending. Alternatively, authenticate with a Bearer API key holding prepaid card credits. One credit per successful scan; failures return the reservation. A successful credit-funded response has no x402 receipt header.",
        "parameters": [
          {
            "in": "header",
            "name": "Idempotency-Key",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{16,128}$"
            }
          },
          {
            "in": "header",
            "name": "PAYMENT-SIGNATURE",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Base64 JSON x402 v2 PaymentPayload. Omit to obtain a price challenge. Omit when using prepaid Bearer credits."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "image_base64",
                  "mime_type"
                ],
                "properties": {
                  "image_base64": {
                    "type": "string",
                    "contentEncoding": "base64",
                    "maxLength": 2666668,
                    "description": "JPEG or PNG, max 2 MB decoded, 16 to 2048 pixels per side."
                  },
                  "mime_type": {
                    "type": "string",
                    "enum": [
                      "image/jpeg",
                      "image/png"
                    ]
                  },
                  "language": {
                    "type": "string",
                    "enum": [
                      "en",
                      "lt"
                    ],
                    "default": "en"
                  },
                  "known_total_grams": {
                    "type": "number",
                    "minimum": 1,
                    "maximum": 5000,
                    "description": "Optional measured weight of all edible food detected in the image, excluding plate and inedible parts. At most two decimal places. Total weight is preserved; individual foods retain photo-estimated proportions. Omit when unknown or when only part of the pictured meal was weighed."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validated nutrition estimate; payment settled",
            "headers": {
              "PAYMENT-RESPONSE": {
                "description": "Base64 JSON x402 settlement receipt",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "request_id",
                    "dish_name",
                    "items",
                    "totals",
                    "estimated_total_grams",
                    "estimate",
                    "model",
                    "matcher_version",
                    "processing_ms"
                  ],
                  "properties": {
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "dish_name": {
                      "type": "string"
                    },
                    "items": {
                      "type": "array",
                      "maxItems": 20,
                      "items": {
                        "type": "object",
                        "required": [
                          "name",
                          "query",
                          "estimated_grams",
                          "model_confidence",
                          "nutrition_source",
                          "fdc_id",
                          "matched_name",
                          "per_100g",
                          "nutrition"
                        ],
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "query": {
                            "type": "string"
                          },
                          "estimated_grams": {
                            "type": "number",
                            "minimum": 0.01,
                            "description": "Estimated edible weight. With known_total_grams, allocated from the supplied total using photo-estimated proportions."
                          },
                          "model_confidence": {
                            "type": "number",
                            "minimum": 0,
                            "maximum": 1,
                            "description": "Uncalibrated model rating."
                          },
                          "nutrition_source": {
                            "type": "string",
                            "enum": [
                              "usda",
                              "ai"
                            ]
                          },
                          "fdc_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "matched_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "per_100g": {
                            "type": "object",
                            "required": [
                              "kcal",
                              "protein_g",
                              "carbs_g",
                              "fat_g"
                            ],
                            "properties": {
                              "kcal": {
                                "type": "number",
                                "minimum": 0
                              },
                              "protein_g": {
                                "type": "number",
                                "minimum": 0
                              },
                              "carbs_g": {
                                "type": "number",
                                "minimum": 0
                              },
                              "fat_g": {
                                "type": "number",
                                "minimum": 0
                              }
                            }
                          },
                          "nutrition": {
                            "type": "object",
                            "required": [
                              "kcal",
                              "protein_g",
                              "carbs_g",
                              "fat_g"
                            ],
                            "properties": {
                              "kcal": {
                                "type": "number",
                                "minimum": 0
                              },
                              "protein_g": {
                                "type": "number",
                                "minimum": 0
                              },
                              "carbs_g": {
                                "type": "number",
                                "minimum": 0
                              },
                              "fat_g": {
                                "type": "number",
                                "minimum": 0
                              }
                            }
                          }
                        }
                      }
                    },
                    "totals": {
                      "type": "object",
                      "required": [
                        "kcal",
                        "protein_g",
                        "carbs_g",
                        "fat_g"
                      ],
                      "properties": {
                        "kcal": {
                          "type": "number",
                          "minimum": 0
                        },
                        "protein_g": {
                          "type": "number",
                          "minimum": 0
                        },
                        "carbs_g": {
                          "type": "number",
                          "minimum": 0
                        },
                        "fat_g": {
                          "type": "number",
                          "minimum": 0
                        }
                      }
                    },
                    "estimated_total_grams": {
                      "type": "number"
                    },
                    "estimate": {
                      "const": true
                    },
                    "model": {
                      "type": "string"
                    },
                    "matcher_version": {
                      "type": "string"
                    },
                    "processing_ms": {
                      "type": "integer",
                      "description": "Nutrition pipeline elapsed time, excluding settlement."
                    },
                    "portion_basis": {
                      "type": "object",
                      "additionalProperties": false,
                      "required": [
                        "type",
                        "total_grams",
                        "allocation"
                      ],
                      "description": "Present only when the caller supplies known_total_grams. This does not make food identities, individual component weights or nutrients measured facts.",
                      "properties": {
                        "type": {
                          "const": "supplied_total"
                        },
                        "total_grams": {
                          "type": "number",
                          "minimum": 1,
                          "maximum": 5000
                        },
                        "allocation": {
                          "const": "photo_estimated_proportions"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "x402 PaymentRequired with an accepts array, or invalid payment",
            "headers": {
              "PAYMENT-REQUIRED": {
                "schema": {
                  "type": "string"
                },
                "description": "Base64 JSON x402 v2 PaymentRequired"
              }
            }
          },
          "400": {
            "description": "Invalid input, unpaid"
          },
          "403": {
            "description": "Untrusted browser Origin"
          },
          "409": {
            "description": "in_progress or idempotency_conflict"
          },
          "410": {
            "description": "Result recovery expired after 24 hours"
          },
          "413": {
            "description": "Image or request too large, unpaid"
          },
          "415": {
            "description": "Use application/json"
          },
          "422": {
            "description": "No food detected, unpaid. Includes request_id for support when analysis was reserved."
          },
          "429": {
            "description": "rate_limited, failure_limit or spend_limited. No payment or credit is consumed. Back off using Retry-After; it is a retry hint, not a guaranteed recovery time. Existing completed requests can still be replayed.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds before retrying: 3600 for failure_limit or spend_limited, otherwise 60."
              }
            }
          },
          "502": {
            "description": "Analysis failed, unpaid. Includes request_id for support when analysis was reserved."
          },
          "503": {
            "description": "Read error.code. payment_pending: retry the SAME authorization and request after 60 seconds."
          },
          "504": {
            "description": "Analysis timed out, unpaid"
          }
        },
        "x-payment": {
          "protocol": "x402",
          "version": 2,
          "scheme": "exact",
          "network": "eip155:8453",
          "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
          "amount": "10000"
        },
        "security": [
          {},
          {
            "CreditKey": []
          }
        ]
      }
    },
    "/v1/credits/balance": {
      "get": {
        "summary": "Read remaining scan credits and whether the key is paused",
        "security": [
          {
            "CreditKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Success. See developer guide for response fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "credits_remaining",
                    "blocked",
                    "test_mode"
                  ],
                  "properties": {
                    "credits_remaining": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "blocked": {
                      "type": "boolean"
                    },
                    "test_mode": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key"
          },
          "409": {
            "description": "Order conflict or payment unconfirmed"
          },
          "429": {
            "description": "Capacity limit"
          },
          "503": {
            "description": "Retry the same order or payment details"
          }
        }
      }
    },
    "/v1/credits/checkout": {
      "post": {
        "summary": "Create a $5 Stripe Checkout link for 500 scan credits. Cardholder completes checkout.",
        "security": [
          {
            "CreditKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Success. See developer guide for response fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "order_id"
                  ],
                  "properties": {
                    "order_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "checkout_url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "session_id": {
                      "type": "string"
                    },
                    "credits": {
                      "type": "integer",
                      "const": 500
                    },
                    "amount_usd": {
                      "type": "string",
                      "const": "5.00"
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "paid",
                        "reversed"
                      ],
                      "description": "Previously completed order. No new checkout was created."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key"
          },
          "409": {
            "description": "Order conflict or payment unconfirmed"
          },
          "429": {
            "description": "Capacity limit"
          },
          "503": {
            "description": "Retry the same order or payment details"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "order_id"
                ],
                "properties": {
                  "order_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "UUID v4. Persist and reuse with the same API key on checkout retries."
                  },
                  "context": {
                    "type": "object",
                    "additionalProperties": false,
                    "description": "Optional self-reported categories. Omit unknown values. First stored context wins on checkout retries.",
                    "properties": {
                      "channel": {
                        "type": "string",
                        "enum": [
                          "website",
                          "node_quickstart",
                          "agent",
                          "unspecified"
                        ]
                      },
                      "discovery": {
                        "type": "string",
                        "enum": [
                          "search",
                          "ai_assistant",
                          "article",
                          "referral",
                          "other",
                          "unspecified"
                        ]
                      },
                      "use_case": {
                        "type": "string",
                        "enum": [
                          "food_logging",
                          "agent_tool",
                          "meal_planning",
                          "research",
                          "other",
                          "unspecified"
                        ]
                      }
                    }
                  }
                },
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/credits/confirm": {
      "post": {
        "summary": "Verify a paid Stripe checkout and recover the credit balance",
        "security": [
          {
            "CreditKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Success. See developer guide for response fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "credits_remaining",
                    "blocked",
                    "test_mode"
                  ],
                  "properties": {
                    "credits_remaining": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "blocked": {
                      "type": "boolean"
                    },
                    "test_mode": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key"
          },
          "409": {
            "description": "Order conflict or payment unconfirmed"
          },
          "429": {
            "description": "Capacity limit"
          },
          "503": {
            "description": "Retry the same order or payment details"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "session_id"
                ],
                "properties": {
                  "session_id": {
                    "type": "string",
                    "pattern": "^cs_live_[A-Za-z0-9]+$"
                  }
                },
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/credits/feedback": {
      "post": {
        "summary": "Submit actual feedback for a completed credit-funded request owned by this key",
        "description": "One category/note per request, at most 10 per key and 1000 globally per rolling 24 hours. Exact repeats are idempotent. The request must have paid or failed status and have been created within 90 days. Feedback expires after 90 days on the next eligible submission. Does not change credits, refunds or models. Card keys only; MCP callers use this REST endpoint.",
        "security": [
          {
            "CreditKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "request_id",
                  "category"
                ],
                "properties": {
                  "request_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "helpful",
                      "wrong_food",
                      "wrong_portion",
                      "wrong_nutrition",
                      "integration_problem",
                      "other"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 500,
                    "default": "",
                    "description": "Optional actual feedback. Never include API keys or personal information."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Invalid category, note, UUID or unexpected fields"
          },
          "401": {
            "description": "Invalid key or payment environment"
          },
          "404": {
            "description": "No eligible completed request owned by this key"
          },
          "409": {
            "description": "A different feedback submission already exists"
          },
          "429": {
            "description": "Feedback limit; respect Retry-After"
          },
          "503": {
            "description": "Storage unavailable; retry the same submission"
          },
          "200": {
            "description": "Recorded, or an identical submission already exists.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "recorded",
                    "duplicate",
                    "request_id"
                  ],
                  "properties": {
                    "recorded": {
                      "type": "boolean",
                      "const": true
                    },
                    "duplicate": {
                      "type": "boolean"
                    },
                    "request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/credits/checkout-status": {
      "post": {
        "summary": "Inspect the original checkout before sending another payment link",
        "description": "Looks up an owned order and, when pending with a known session, reads Stripe state. Recovers verified paid credits even before a webhook. Does not create a new checkout or expire an existing one. A pending order without a saved session is unresolved; retry its original checkout order, never replace it based on local time. Only paid or Stripe-confirmed expired state permits an explicit new order in the quickstart. For a session-less old order, recovery pages through the entire possible Stripe creation interval, bounded at five pages. Only a complete empty listing after 48 hours returns not_issued; partial or failed lookup remains unresolved. not_issued permits an explicit replacement. These checks concern the specified order, not independently created orders from other clients.",
        "security": [
          {
            "CreditKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "order_id"
                ],
                "properties": {
                  "order_id": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Owned order status and current balance.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "order_id",
                    "state",
                    "credits_remaining",
                    "blocked",
                    "test_mode"
                  ],
                  "properties": {
                    "order_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "state": {
                      "type": "string",
                      "enum": [
                        "open",
                        "paid",
                        "expired",
                        "unresolved",
                        "reversed",
                        "not_issued"
                      ]
                    },
                    "credits_remaining": {
                      "type": "integer"
                    },
                    "blocked": {
                      "type": "boolean"
                    },
                    "test_mode": {
                      "type": "boolean"
                    },
                    "checkout_url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No order owned by this key"
          },
          "401": {
            "description": "Invalid API key"
          },
          "503": {
            "description": "Storage or Stripe unavailable; retry the same order"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "CreditKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "kcs_live_<64 lowercase hex characters>",
        "description": "Prepaid card credits: $5 for 500 successful scans at https://kcalsnap.com/developers/credits/."
      }
    }
  }
}
