← Leaf Disease ID/API
Your token

Leaf Disease ID API

The free leaf check runs entirely in the browser and has no API: it is a ViT-base/16 model plus the 38-condition layer in /head.json, all served with the page. What you can call from code is the paid Expert check: one leaf photo goes to an OpenAI vision model (gpt-terra, served by SkillSafe), which returns the crop, the most likely problem, what in the photo shows it, look-alikes and what to do, as one JSON object.

Base URL https://api.skillsafe.ai/v1/app-api. Every response is {"ok":true,"data":…} or {"ok":false,"error":{"code","message"}}. A run is paid with SkillSafe credits from the signed-in account; /me and /estimate are free. A first opinion from a photo, not a diagnosis.

Errors

HTTPcodemeaning
400validation_errorBody is not a JSON object, a field has the wrong type, or task / local_top3 is missing.
401unauthorizedMissing or expired token, or a guest token on a run: sign in for a personal token.
402insufficient_creditsBalance below the run's minimum. Top up at skillsafe.ai.
404not_foundA $files id that does not exist or belongs to another token.
429rate_limitedSlow down and retry with the same Idempotency-Key.

1. Get a token

Open the token page, sign in, and copy your token. A guest token can call /me and /estimate but cannot run.

2. Check the session — GET /me

curl -s https://api.skillsafe.ai/v1/app-api/me \
  -H "Authorization: Bearer YOUR_TOKEN"
import requests
H = {"Authorization": "Bearer YOUR_TOKEN"}
print(requests.get("https://api.skillsafe.ai/v1/app-api/me", headers=H).json()["data"])
const H = { Authorization: "Bearer YOUR_TOKEN" };
const me = await (await fetch("https://api.skillsafe.ai/v1/app-api/me", { headers: H })).json();
console.log(me.data.subject_type, me.data.credits);

3. Upload the leaf — POST /files

Multipart, field file. Crop to a square around one leaf and shrink it first: the page sends at most 768 px, JPEG, without EXIF. File ids belong to the token that uploaded them.

curl -s -X POST https://api.skillsafe.ai/v1/app-api/files \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "file=@leaf.jpg;type=image/jpeg"
# -> {"ok":true,"data":{"file":{"file_id":"fil_...", ...}}}
with open("leaf.jpg", "rb") as f:
    r = requests.post("https://api.skillsafe.ai/v1/app-api/files", headers=H,
                      files={"file": ("leaf.jpg", f, "image/jpeg")})
file_id = r.json()["data"]["file"]["file_id"]
const fd = new FormData();
fd.append("file", leafBlob, "leaf.jpg");
const up = await (await fetch("https://api.skillsafe.ai/v1/app-api/files", { method: "POST", headers: H, body: fd })).json();
const fileId = up.data.file.file_id;

4. Input fields

All values are strings; local_top3 is JSON text. Without a local result, send "[]" and an empty local_verdict.

fieldtyperequiredmeaning
taskstringyesAlways "expert_check".
local_top3stringyesJSON array of up to 3 {"crop","condition","confidence"} from the free check, e.g. [{"crop":"Tomato","condition":"Late blight","confidence":"81%"}]. The model judges the photo itself and then says whether it agrees.
local_verdictstringnoThe free check's verdict: confident, likely, uncertain, unfamiliar or not-leaf.
crop_hintstringnoThe plant, if known, e.g. "Tomato". A strong hint, not a fact.
colour_mapstringnoShare of the leaf that looks yellowing, brown or pale, e.g. "34% discoloured (yellowing 12%, brown 22%); leaf fills 61% of the square".
notestringnoUp to 400 characters from the grower: where it grows, when it started. Treated as data.
$filesarrayyesOne file id from step 3.

5. Price it — POST /estimate (free)

hold_credits is the most the run can reserve; you are charged charged_credits, usually far less. 10,000 credits = $1.

curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
  -H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" \
  -d '{"task":"expert_check","crop_hint":"Tomato","local_verdict":"likely","local_top3":"[{\"crop\":\"Tomato\",\"condition\":\"Late blight\",\"confidence\":\"81%\"}]","colour_map":"","note":"","$files":["FILE_ID"]}'
import json
top3 = [{"crop": "Tomato", "condition": "Late blight", "confidence": "81%"}]
body = {"task": "expert_check", "crop_hint": "Tomato", "local_verdict": "likely",
        "local_top3": json.dumps(top3), "colour_map": "", "note": "", "$files": [file_id]}
print(requests.post("https://api.skillsafe.ai/v1/app-api/estimate", headers=H, json=body).json()["data"])
const top3 = [{ crop: "Tomato", condition: "Late blight", confidence: "81%" }];
const body = { task: "expert_check", crop_hint: "Tomato", local_verdict: "likely",
  local_top3: JSON.stringify(top3), colour_map: "", note: "", $files: [fileId] };
const est = await fetch("https://api.skillsafe.ai/v1/app-api/estimate", { method: "POST",
  headers: { ...H, "Content-Type": "application/json" }, body: JSON.stringify(body) });
console.log((await est.json()).data);

6. Run — POST /run, then poll GET /jobs/{id}

The body is the same object as the estimate. Send an Idempotency-Key: a retry with the same key never bills twice.

curl -s -X POST https://api.skillsafe.ai/v1/app-api/run \
  -H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: leaf-2026-11-01-001" \
  -d @body.json   # body.json = the object from step 5
# -> {"ok":true,"data":{"job_id":"job_...", ...}}
curl -s https://api.skillsafe.ai/v1/app-api/jobs/JOB_ID -H "Authorization: Bearer YOUR_TOKEN"
# repeat until data.status is "succeeded" or "failed"; the reply text is data.output.output
import time
r = requests.post("https://api.skillsafe.ai/v1/app-api/run",
                  headers={**H, "Idempotency-Key": "leaf-2026-11-01-001"}, json=body)
job_id = r.json()["data"]["job_id"]
while True:
    job = requests.get(f"https://api.skillsafe.ai/v1/app-api/jobs/{job_id}", headers=H).json()["data"]
    if job["status"] in ("succeeded", "failed"):
        break
    time.sleep(1.5)
answer = json.loads(job["output"]["output"])
print(answer["summary"], job["charged_credits"])
const run = await fetch("https://api.skillsafe.ai/v1/app-api/run", { method: "POST",
  headers: { ...H, "Content-Type": "application/json", "Idempotency-Key": "leaf-2026-11-01-001" },
  body: JSON.stringify(body) });
const jobId = (await run.json()).data.job_id;
let job;
do {
  await new Promise((r) => setTimeout(r, 1500));
  job = (await (await fetch(`https://api.skillsafe.ai/v1/app-api/jobs/${jobId}`, { headers: H })).json()).data;
} while (job.status !== "succeeded" && job.status !== "failed");
const answer = JSON.parse(job.output.output);
console.log(answer.summary, job.charged_credits);

Output contract

output.output is one JSON object. If a reply is cut off, the page closes the open brackets and shows what arrived; do the same, or retry.

{
  "photo": {"status": "ok", "note": "Sharp, evenly lit upper surface of one leaflet."},
  "summary": "Most likely late blight on a tomato leaf; fairly sure.",
  "crop": {"name": "Tomato", "confidence": "high"},
  "problem": {"name": "Late blight", "category": "disease",
              "cause": "Phytophthora infestans (oomycete)", "confidence": "medium"},
  "evidence": ["Large greasy dark-brown patches not limited by veins", "Pale green margin around the lesions"],
  "alternatives": [{"name": "Early blight", "category": "disease",
                    "tell_apart": "Early blight spots are smaller, with concentric rings, on older leaves first."}],
  "management": {"cultural": ["Remove and bag affected leaves", "Water at the base, in the morning"],
                 "options": ["Protectant fungicides labelled for late blight on tomato"]},
  "extension": "Contact your extension service now: late blight spreads fast and may need reporting.",
  "local_check": {"agrees": "yes", "note": "Matches the free check's first answer."}
}
keyvalues
photo.statusok, poor, too_blurry, not_leaf. On not_leaf, crop and problem are null.
problem.categorydisease, pest, nutrient, abiotic, healthy, unknown.
*.confidencehigh, medium, low.
local_check.agreesyes, partly, no, n/a.
management.optionsProduct groups only: never doses, brand names or spray intervals, which differ by country and crop.

Check what is legal for your crop and country with a local extension service or plant clinic before buying or spraying anything.