Identify a tree, a piece of fruit or an animal from a photograph of it — no tag, no QR code, nothing attached to the thing itself. Any application can call it: one account, one token, one POST with an image.
These run on any machine with curl. No SDK, and no key that OriLife has to issue by hand.
curl https://api.orilife.io/api/health
Returns features — a capability list generated from the live route table.
Read that array to decide which screens to show; do not hard-code a feature list into your app.
curl -X POST https://api.orilife.io/api/signup \
-H 'Content-Type: application/json' \
-d '{"username":"my_orchard","password":"durian.season.2026"}'
Returns {"ok":true,"token":"…","account":{…}}. If you already have an account,
change /api/signup to /api/login — the request body is identical.
The username and password rules, because this is where a first call most often gets rejected:
the username is 3–32 characters and may contain only lowercase letters, digits, dots and underscores —
no hyphens. The password is at least 10 characters, must mix at least two character classes, and must
not appear in the common-password list. A violation returns 400 naming the specific rule.
curl -X POST https://api.orilife.io/api/identify/auto \ -H "Authorization: Bearer $TOKEN" \ -F 'files=@photo.jpg' -F 'lat=10.762' -F 'lon=106.660'
You do not have to declare whether you photographed a tree, a fruit or an animal — the server
works that out and then identifies the individual. The response carries kind (what it recognised),
lane (which route ran) and result, which is that route's own response verbatim.
If you already know the kind, call /api/identify, /api/fruit/identify or
/api/animal/identify directly and save a step.
For several images, repeat -F 'files=@…'. Coordinates are optional but worth sending —
they narrow the search area and make the traceability code more stable.
Identification here means identifying an individual: not "this is a durian tree" but "this is tree number 47 in this orchard". The same method applies to any living thing whose surface is distinctive enough, which is why entity kind is a row in the table below rather than a separate product.
| Kind | Enrol | Re-identify | Status |
|---|---|---|---|
| Tree | POST /api/enroll | POST /api/identify | Running in the field |
| Fruit | POST /api/fruit/enroll | POST /api/fruit/identify | Running in the field; the match label is not yet calibrated, and it is not yet on the evidence path below |
| Animal | POST /api/animal/enroll | POST /api/animal/identify | Working; the route still requires species and farm_id |
| Flowers, processed goods | none yet | No route — the auto door returns "not recognised" rather than guessing a class | |
How to read a fruit match. Fruit re-identification is live, but its confidence label is not yet
calibrated. On a leave-one-view-out run over the fruit enrolled so far, almost every query came back labelled
as a confident match, and the score itself barely separated the right answers from the wrong ones. Build the
screen as a short candidate list with photographs and let the person choose, rather than as a single claim.
Pass tree_id whenever the application knows which tree the fruit came from: it narrows the search
pool, and it is the largest single improvement available today. Tree re-identification does not carry this
caveat.
This table is not hard-coded into the page: the same list is served at
/.well-known/orilife.json and is generated from the server's real route table, so it cannot
promise a door the code does not have.
A shopper photographs a piece of fruit on display, or scans a code on a receipt, and traces its origin. No sign-in, and nothing about them is retained.
POST /api/fruit/lookup — one fruit photo → up to 5 candidates with images, within the public fruit setPOST /api/fruit/scan → /api/fruit/scan/choose — the point-of-sale scanning flowGET /api/resolve/{code} — resolve a traceability codeGET /api/species/catalog — the species catalogueGET /api/health, GET /api — health and route indexEnrol trees, re-identify them, record care work, build 3D captures, anchor evidence. Each account only ever matches within its own orchard.
POST /api/enroll — enrol one tree from a handful of photosPOST /api/identify · POST /api/identify/video — re-identifyPOST /api/verify_add — confirm the match, then add the new angle to that treePOST /api/fruit/enroll · /api/fruit/identify — fruitPOST /api/animal/enroll · /api/animal/identify — livestockGET /api/{entity_type}/{id}/timeline — a hash-chained timelineSend the token as a header on every call that needs authorisation:
Authorization: Bearer <token>
| What you want | Route |
|---|---|
| Open an account | POST /api/signup |
| Sign in | POST /api/login |
| See who you are | GET /api/me |
| Revoke tokens on every device | POST /api/logout-all |
| Sign in with a PhoenixKey biometric key | GET /api/auth/did/challenge → POST /api/auth/did/verify |
A token lives 12 hours by default. A same-origin web app may use a session cookie instead of the header; native apps should use Bearer — it is immune to CSRF.
Every route returns JSON and always carries an ok field. On failure, ok:false
comes with error, a complete sentence.
{"ok": false, "error": "Cần đăng nhập.", "detail": "Cần đăng nhập."}
error and detail are currently
written in Vietnamese, because every user in the field today is Vietnamese. They are safe to log and safe to
branch on via the status code, but do not pipe them straight to an English-speaking user. Branch on the HTTP
status in the table below and write your own copy.
| Code | Meaning | What the app should do |
|---|---|---|
401 | Not signed in, or the token expired | Sign in again and resume the interrupted action |
403 | Right person, no rights over this object | Do not retry — say so plainly |
404 | Does not exist, or exists and you are not allowed to know that | Show "not found" and infer nothing further |
413 | File over the limit | Compress and resend — see the limits below |
422 | Missing field or wrong type | Read detail to learn which field |
429 | Calling too fast | Read the Retry-After header (seconds) and wait. Do NOT discard the user's photo |
| Kind | Limit | Applies to |
|---|---|---|
| Single file | 20 MB | Every single-file route |
| Image batch | 64 MB | /api/enroll, /api/identify and other multi-image routes |
| Video | 80 MB | /api/identify/video, /api/tree/{id}/video, /api/tree/{id}/fruit_video |
Going over returns 413 at the body-reading layer — the server does not load the whole
file before refusing it, so an app on a weak connection does not waste bandwidth.
Every tree has a human-readable code shaped ORI-<geohash7>-<8 characters>
— for example ORI-w3gv5j2-A7K9PQ2M. It is stable for that tree and that coarse location, it prints
on paper, and it is the same string that appears in the data anchored on Cardano.
| State | Meaning | What comes back |
|---|---|---|
public | The owner has published it | Provenance record plus a link to /t/{code} |
restricted | Real, but only the minimum is revealed | A summary card — no images, no coordinates |
unknown | Not resolvable | 404 with a fixed body — no reason given |
unknown gives no reason: if "wrong code" and "correct code but private" answered
differently, a stranger enumerating codes could learn which trees exist. All three states pass through one
decision function shared by the HTML page and the API — two independent implementations always drift, and the
wider of the two is always where the leak is.
Identification is only step one. Each tree has a canonical record that is hashed and anchored on Cardano, while the images live on LampNet under a content address. Anyone can re-check it without trusting OriLife.
Which kinds are on this path today. Trees, but only as far as first enrolment: the record is hashed,
anchored on Cardano, and the images are addressed on LampNet for the photographs sent with
POST /api/enroll when the tree is created. Photographs added to that tree afterwards, through
/api/enroll again, /api/verify_add or /api/tree/{id}/video, are stored
and used for recognition but are held back from the anchored record by the same switch described below. So an
anchored tree record can name fewer images than the server actually holds for that tree; read the count in
GET /api/provenance/{tree_id} rather than assuming it covers everything. For fruit and for farm
boundaries nothing is on this path at all: the records are identified and stored on the server, but they are
not pushed to LampNet and not anchored. The code for all of these paths exists; the switch that enables it is
off on the live server. While it is off, read a fruit identification as an answer from OriLife rather than as
evidence you can re-check independently. One fruit path is already distributed: video of fruit on a tree
(/api/tree/{id}/fruit_video) is stored on LampNet and returns a content address.
GET /api/provenance/{tree_id} | Code, image CIDs, record CID, hash, anchor status |
GET /api/anchor/status | Health of the anchoring path — token required |
GET /api/{entity_type}/{id}/proof/{event_id} | The Merkle path proving one event belongs to an anchored root |
POST /api/animal/trust/verify-inclusion | Check a record against an anchored Merkle root — open to anyone |
/api/health returns features, version and commit.
That list is generated from the real route table, so it is never stale. Read it, then decide which screens to
show — that way a new server capability is usable immediately, with no app release.
Matching is scoped to the account holder, never across the whole system. That is both a privacy
property and an accuracy one: two trees of the same species in two different provinces never get the chance to
be confused. To share access with someone else use POST /api/grant — never hand over a password.
CORS is open to every origin. In exchange, cross-origin cookies are not sent, so the key
must travel in the Authorization: Bearer … header — an unfamiliar web page holds no token of your
user's, cannot borrow anyone's identity, and the whole CSRF class disappears. Native apps (Swift, Kotlin,
React Native, Flutter) are unaffected by CORS and need no changes.
People writing applications have /docs. Machine readers — a marketplace bot, an agent
told to "check this shipment for me", a device out in the field — need a short manifest telling them what this
place is before they fetch a long specification.
GET /.well-known/orilife.json | Service manifest: which routes need no key, what the limits are, which entity kinds have a route |
GET /llms.txt | A one-page map for language agents |
GET /openapi.json | Per-route specification; generates a client |
GET /robots.txt | Public surface open to crawling, operational surface not |
Three things an agent should know up front: unknown on a code lookup carries
no reason (if "wrong code" differed from "private code", enumeration would count someone else's trees);
uncertain is a real result, not an error to retry; and 429 always carries
Retry-After — read it and wait exactly that long.