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, and must not appear in the common-password list. 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; the route still requires species and farm_id
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.

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.

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. Each account only ever matches within its own orchard.

  • 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/challengePOST /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

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."}
Worth knowing before you wire this into a UI: 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.
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
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 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.

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.

Each account sees only its own orchard

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.

Browsers can call it directly; no origin allow-listing

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.

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