OriLife API

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.

https://api.orilife.io
Reading server status…

Your first three calls

These run on any machine with curl. No SDK, and no key that OriLife has to issue by hand.

1 · What the server is, and what it can do

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.

2 · Open an account, get a token

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.

3 · Send an image, get an identity back

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.

One door for every kind of entity

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.

KindEnrolRe-identifyStatus
TreePOST /api/enrollPOST /api/identifyRunning in the field
FruitPOST /api/fruit/enrollPOST /api/fruit/identifyRunning in the field; the match label is not yet calibrated, and it is not yet on the evidence path below
AnimalPOST /api/animal/enrollPOST /api/animal/identifyWorking; 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 goodsnone yetNo 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.

Two ways in — pick by the kind of application you are building

NO ACCOUNT NEEDED

The public lane — for buyer-facing apps

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 set
  • POST /api/fruit/scan → /api/fruit/scan/choose — the point-of-sale scanning flow
  • GET /api/resolve/{code} — resolve a traceability code
  • GET /api/species/catalog — the species catalogue
  • GET /api/health, GET /api — health and route index
TOKEN REQUIRED

The grower lane — for production apps

Enrol 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 photos
  • POST /api/identify · POST /api/identify/video — re-identify
  • POST /api/verify_add — confirm the match, then add the new angle to that tree
  • POST /api/fruit/enroll · /api/fruit/identify — fruit
  • POST /api/animal/enroll · /api/animal/identify — livestock
  • GET /api/{entity_type}/{id}/timeline — a hash-chained timeline

Authentication

Send the token as a header on every call that needs authorisation:

Authorization: Bearer <token>
What you wantRoute
Open an accountPOST /api/signup
Sign inPOST /api/login
See who you areGET /api/me
Revoke tokens on every devicePOST /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.

Response shape and error shape

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"}
Worth knowing before you wire this into a UI: branch on 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.
The routes with no 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.
CodeMeaningWhat the app should do
401Not signed in, or the token expiredSign in again and resume the interrupted action
403Right person, no rights over this objectDo not retry — say so plainly
404Does not exist, or exists and you are not allowed to know thatShow "not found" and infer nothing further
413File over the limitCompress and resend — see the limits below
422Missing field or wrong typeRead detail to learn which field
429Calling too fastRead the Retry-After header (seconds) and wait. Do NOT discard the user's photo

Upload limits

KindLimitApplies to
Single file20 MBEvery single-file route, except the two public fruit routes below
Public fruit photo2 MB/api/fruit/lookup, /api/fruit/scan (no login; kept small to protect the server during fairs)
Image batch64 MB/api/enroll, /api/identify and other multi-image routes
Video80 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.

Traceability codes, and the three states

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.

StateMeaningWhat comes back
publicThe owner has published itProvenance record plus a link to /t/{code}
restrictedReal, but only the minimum is revealedA summary card — no images, no coordinates
unknownNot resolvable404 with a fixed body — no reason given
Why 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.

Evidence: what happens after identification

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/statusHealth 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-inclusionCheck a record against an anchored Merkle root — open to anyone

Three things to know before your first line of code

Read the capabilities, do not hard-code them

/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.

What a match can land on: your trees, shared trees, public trees

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.

Calling from a web page on another origin: off unless this server lists your origin

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.

For bots, agents and devices

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.jsonService manifest: which routes need no key, what the limits are, which entity kinds have a route
GET /llms.txtA one-page map for language agents
GET /openapi.jsonPer-route specification; generates a client
GET /robots.txtPublic 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.

Full documentation