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, must
not appear in the common-password list, and must not be the username or a close variant of it (the
username in capitals or with a few characters added). 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; farm_id is required. species is optional: when it is missing the server reads it from the first photo, and if it cannot, the animal is saved as unknown, which then stays part of its identifier. Send species when the farmer knows it |
| 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.
The status column above is written by hand. The machine-readable list of entity kinds that have a
route is served at /.well-known/orilife.json (entity_kinds): the server builds it by
checking each kind's identify route against its own route table, so that list cannot name 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. Tree matching covers the account's own trees, trees shared with it, and other owners' public trees. Other people's private trees are never matched.
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.
Routes answer JSON, apart from the ones that hand back an image or a file. Success bodies are not
uniform: list reads such as GET /api/trees and GET /api/tree_views carry no
ok at all, and a few routes answer HTTP 200 with ok:false (for example
POST /api/rename when the new name is blank after trimming, that is only spaces; a name sent
completely empty counts as a missing field and gets 422). Check the HTTP status first, then ok on
the routes that have it.
Almost every error (4xx and 5xx) carries ok:false and error, a complete
sentence. A call that needs a token, sent without one (GET /api/trees):
{"ok": false, "error": "Cần đăng nhập.", "detail": "Cần đăng nhập."}
Error sentences are Vietnamese by default, because every user in the field today is Vietnamese.
Send Accept-Language: en and an error that has a translation comes back in English with a stable
code. The same call with that header:
{"ok": false, "error": "Authentication required.", "detail": "Authentication required.", "code": "auth.required"}
code or on the HTTP status in the
table below, never on the sentence. A few error sentences are not translated yet and stay Vietnamese with no
code, and 200 bodies are never translated, so keep your own copy for those cases.
error: a few routes build their own error body around a stable
reason instead, so branch on reason there. POST /api/auth/did/verify (PhoenixKey
sign-in) answers {ok:false, reason, detail} (a missing JSON field still gets the usual 422); its 429,
and the 429 of GET /api/auth/did/challenge, carry only ok:false and detail. The MAGIC and
Covenant routes (/api/magic/*, /api/covenant/*) answer their own errors the same way, each
with its own reason values, and so does the 402 magic_consume_overdue answer of the MAGIC
debt gate, which is off by default.
| 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, except the two public fruit routes below |
| Public fruit photo | 2 MB | /api/fruit/lookup, /api/fruit/scan (no login; kept small to protect the server during fairs) |
| 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. Trees registered from October 2026 get
ORI-TREE-<16 hex characters> — for example ORI-TREE-4C1F0A9B2D7E6358 — which
carries no location. Trees registered earlier keep their original code shaped
ORI-<geohash7>-<8 characters> (for example ORI-w3gv5j2-A7K9PQ2M) forever,
so printed labels and existing anchors stay valid. Either way the code is stable for that tree, it prints
on paper, and it is the same string that appears in the data anchored on Cardano. Accept both shapes.
| 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.
Tree re-identification (POST /api/identify) compares a photo with three groups of
trees: the trees the account owns, trees another owner has shared with it through POST /api/grant,
and every owner's public trees. Other people's private trees are never candidates. Each candidate says which
group it came from in relation (owner, granted or public); a
public tree of another owner comes without its distance (dist_m) or its last-update age
(days). Fruit and animal re-identification
stay within the account's own records. To share your private trees with someone use
POST /api/grant; never hand over a password.
Cross-origin browser calls (CORS) are a per-server setting and are off by default. Read
cors in /.well-known/orilife.json: allow_origins lists the origins a
browser page may call from (an empty list means none), and allow_credentials says whether cookies
travel with those calls. When an operator opens it to every origin ("*"), cookies are never sent,
so the key travels in the Authorization: Bearer … header and an unfamiliar web page cannot borrow
your user's identity. Native apps (Swift, Kotlin, React Native, Flutter) and server-side code 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.