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:

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.

StatusCodes
400consent_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
401missing_api_key, invalid_api_key, capture_link_invalid
402monthly_limit_reached, payment_required
404session_not_found, not_found
409liveness_not_passed, capture_unavailable, capture_already_completed, webhook_not_set
410capture_link_expired
413payload_too_large, photo_too_large
422no_face_in_photo
429rate_limited (see Retry-After)
500, 502, 503internal_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.