Docs
Base URL https://api.tryminno.com. Send your key as Authorization: Bearer mn_... and keep it on your server. Machine-readable spec: openapi.json.
Consent and notice
A liveness check captures a person's face, which biometric laws (Illinois BIPA, Texas, Washington, Quebec Law 25, the GDPR and others) treat as biometric data. You collect it inside your product, so before you call POST /v1/sessions for a person you must show that person a notice and obtain their express written consent. The notice must say:
- that a face liveness check (and a face comparison, if you use it) will be performed;
- who performs it: you, with Minno (Rook Industries, Inc.) and Amazon Web Services as your vendors;
- the purpose of the check;
- how long the data is kept. Rook stores no face image, video or template; it keeps the decision, score and session metadata for 13 months, as the Biometric Data Retention and Destruction Policy describes. Add your own retention period for anything you keep.
Send "consent_obtained": true with every session to record that you did this. Sessions without it are refused with 400. The flag is stored with the session so your own records show consent per check. Our role as your processor is set out in the Data Processing Agreement, which includes a Biometric Addendum. Keep your own consent records and publish your own retention policy; Minno does not collect consent for you.
Get a key
Get a free test key instantly at tryminno.com/start. Test keys return simulated results and never bill. A live key goes through Stripe Checkout: you add a card up front and the first 100 checks are free. The key is shown once; we keep only its fingerprint.
1. Create a session
POST /v1/sessions
{ "consent_obtained": true, // required
"reference": "customer-42", "pass_threshold": 85,
"return_url": "https://app.example.com/verified" } // optional
200 {
"session_id": "4f1c...",
"test": false,
"region": "us-east-1",
"credentials": { "access_key_id": "...", "secret_access_key": "...",
"session_token": "...", "expiration": "2026-11-07T21:15:00Z" },
"capture_url": "https://api.tryminno.com/capture/?s=4f1c...#t=..."
}
reference is your id for the end customer (up to 200 characters); usage is split by it. pass_threshold (0 to 100, default 80) sets the score needed for live: true.
2. Run the capture
Option A: hosted link (no front-end work)
Send the person to capture_url. It carries no Minno brand, works on phones and laptops, and expires after 15 minutes. Treat it like a password: it is the only credential the page has. When the person finishes, they go to your return_url with ?session_id=...&status=... added. The browser never learns the decision; read it from your backend.
Option B: your own screen
Use AWS's open-source liveness component with the credentials (they last 15 minutes and can only stream a liveness capture): @aws-amplify/ui-react-liveness on the web, Amplify UI for Swift on iOS and for Android.
import { FaceLivenessDetectorCore } from "@aws-amplify/ui-react-liveness";
<FaceLivenessDetectorCore
sessionId={s.session_id}
region={s.region}
config={{ credentialProvider: async () => ({
accessKeyId: s.credentials.access_key_id,
secretAccessKey: s.credentials.secret_access_key,
sessionToken: s.credentials.session_token,
}) }}
onAnalysisComplete={() => fetch(`/api/check/${s.session_id}`)}
/>
3. Get the result
GET /v1/sessions/{id} // add ?include_image=true for the reference frame
200 { "session_id": "4f1c...", "reference": "customer-42", "status": "succeeded",
"live": true, "confidence": 97.4, "pass_threshold": 85,
"reference_image": null, "test": false }
status is pending, then exactly one of succeeded, failed (the check could not complete) or expired. live is your decision: the check succeeded and confidence reached pass_threshold. Once a session is final, reads come from our database and are fast. The reference frame is never stored by Minno, so it is only available while AWS still holds it.
4. Webhooks
PUT /v1/webhook { "url": "https://app.example.com/hooks/minno" }
200 { "url": "...", "secret": "whsec_..." } // shown once; PUT again to rotate
GET /v1/webhook // url + last 20 deliveries with status and error
DELETE /v1/webhook
POST /v1/webhook/test // queues a "ping" event
When a session becomes final we POST, whether or not anyone polled:
Minno-Signature: t=1762550000,v1=5f2c...
{ "id": "evt_812", "type": "session.completed", "created": 1762550000,
"data": { "session_id": "4f1c...", "reference": "customer-42", "status": "succeeded",
"live": true, "confidence": 97.4, "pass_threshold": 85, "test": false } }
Verify: v1 is hex HMAC-SHA256 of "<t>.<raw body>" with your secret; reject if t is more than 5 minutes old. Answer 2xx within 10 seconds. Failed deliveries retry with backoff (30 s, 1 m, 2 m, up to 6 h apart) for 10 attempts. Delivery is at least once: dedupe on id. URLs must be https on a public address; redirects are not followed.
5. Match to an ID photo (optional)
POST /v1/sessions/{id}/match
{ "id_photo": "<base64 JPEG or PNG, up to 5 MB>" }
200 { "session_id": "4f1c...", "match": true, "similarity": 98.1, "match_threshold": 90, "test": false }
Refused with 409 liveness_not_passed unless the session passed, so a capture that failed liveness is never matched.
6. Delete a session
DELETE /v1/sessions/{id}
204
Erases the session's decision, score, reference and return URL at once and stops any pending webhook for it. Only the billing count remains (no personal data). Without a request, sessions are deleted 13 months after the check, as the Biometric policy says.
Usage
GET /v1/usage
200 { "month_to_date": { "sessions_created": 120, "checks_billed": 104, "face_matches_billed": 98 },
"by_reference": [ { "reference": "customer-42", "checks": 60, "face_matches": 58 } ],
"monthly_limit": null }
Only succeeded checks bill, whether you read them or not. Failed, expired and abandoned sessions are free.
Billing
$0.05 per check. First 100 free. No minimums. The 100 free checks are per live key and never reset. Only succeeded checks count; face matching is included.
After 100 checks, a live key needs a card. Call POST /v1/billing/checkout with that key: it returns {"url": "https://checkout.stripe.com/..."}. Send the account owner there to add a card (Stripe Checkout); you are then billed monthly for checks past the first 100. Until a card is in good standing, POST /v1/sessions on a live key returns 402 payment_required. A failed payment pauses new checks the same way; paying the invoice resumes them. Cancelling the subscription removes the card; the key keeps working within its free checks. Test keys are never billed.
Test keys
Keys starting mn_test_ never touch a camera or AWS and are never billed. Add "simulate": "fail" to get a failed check; the default is a pass. Test sessions settle at once, fire webhooks and work on the hosted capture link, so your CI can run the whole flow.
Errors
Every error is {"error": {"code": "...", "message": "..."}}. Branch on code; messages may change. Every response carries an x-request-id; quote it when you contact us.
| Status | Codes |
|---|---|
| 400 | consent_required, terms_required, invalid_email, invalid_body, invalid_pass_threshold, invalid_reference, invalid_simulate, simulate_needs_test_key, invalid_return_url, invalid_id_photo, invalid_webhook_url |
| 401 | missing_api_key, invalid_api_key, capture_link_invalid |
| 402 | monthly_limit_reached, payment_required |
| 404 | session_not_found, not_found |
| 409 | liveness_not_passed, capture_unavailable, capture_already_completed, webhook_not_set |
| 410 | capture_link_expired |
| 413 | payload_too_large, photo_too_large |
| 422 | no_face_in_photo |
| 429 | rate_limited (see Retry-After) |
| 500, 502, 503 | internal_error, upstream_error, upstream_busy: retry with backoff |
Limits
25 requests per second per key. Bodies up to 8 MB. Requests time out after 30 seconds.