Capture API
ISO 27001SOC 2 CertifiedGDPR CompliantGASA Partner

TruthScan Capture API

Mint a phone capture link, receive a signed photo, and read the result with your API key.

All endpoints are on https://truthscan.com. Send the end user to captureLink. Your backend never receives the JPEG.

Authentication

Create a secret key in your account.

Send it on every call as the x-api-key header or as Authorization: Bearer. The key must belong to an organization with API access. The capture link opened on the phone does not use this key.

Replace YOUR_API_KEY with your secret key.

Create a session

Returns a capture link. inviteeEmail is required. expiresInSeconds defaults to 15 minutes and cannot exceed 7 days. maxCaptures defaults to 20 and cannot exceed 20. webhookUrl is optional. When set, it must be https and must resolve to a public address. webhookSecret is returned only in this response.

POST /api/capture/session/create

Set sendEmail to true to have TruthScan email the link. Then subject and message are required. Put {link} in the message where the capture URL should appear. emailSent is true only when that email went out. Otherwise send captureLink yourself.

Store webhookSecret. It is the HMAC key for capture.completed.

Example request

curl -X POST https://truthscan.com/api/capture/session/create \
  -H 'Content-Type: application/json' \
  -H 'x-api-key: YOUR_API_KEY' \
  -d '{
    "inviteeEmail": "person@example.com",
    "expiresInSeconds": 900,
    "maxCaptures": 1,
    "webhookUrl": "https://example.com/hooks/capture"
  }'

Example response

{
  "sessionId": "11111111-1111-4111-8111-111111111111",
  "captureToken": "…",
  "captureUrl": "/capture/11111111-1111-4111-8111-111111111111?token=…",
  "captureLink": "https://truthscan.com/capture/11111111-1111-4111-8111-111111111111?token=…",
  "expiresAt": "2026-10-07T16:00:00.000Z",
  "expiresInSeconds": 900,
  "maxCaptures": 1,
  "webhookSecret": "…",
  "emailSent": false
}

Read results

Poll the session until capturesUsed increases, or wait for the webhook. Download URLs expire after one hour.

Session status

Expiry, captures used, and the webhook URL you registered.

GET /api/capture/sessions/{sessionId}

Example request

curl https://truthscan.com/api/capture/sessions/SESSION_ID \
  -H 'x-api-key: YOUR_API_KEY'

Example response

{
  "sessionId": "11111111-1111-4111-8111-111111111111",
  "expiresAt": "2026-10-07T16:00:00.000Z",
  "maxCaptures": 1,
  "capturesUsed": 1,
  "inviteeEmail": "person@example.com",
  "webhookUrl": "https://example.com/hooks/capture",
  "createdAt": "2026-10-07T15:45:00.000Z"
}

List photos

Photos for one session, newest first. Query page and limit (max 50). Each item is a summary plus downloadUrl. The list location only includes latitude, longitude, and altitude. Fetch the asset for accuracy, heading, and speed.

GET /api/capture/sessions/{sessionId}/assets

Example request

curl 'https://truthscan.com/api/capture/sessions/SESSION_ID/assets?page=1&limit=20' \
  -H 'x-api-key: YOUR_API_KEY'

Example response

{
  "assets": [
    {
      "id": "22222222-2222-4222-8222-222222222222",
      "verificationState": "trusted",
      "trustTier": "standard",
      "risk": { "score": 0.95, "flags": ["geolocation_low_accuracy"] },
      "capturedAtUtc": "2026-10-07T15:50:00.000Z",
      "submittedAtUtc": "2026-10-07T15:50:01.200Z",
      "signedAtUtc": "2026-10-07T15:50:03.000Z",
      "captureToSubmitMs": 1200,
      "geolocation": {
        "latitude": 37.7749,
        "longitude": -122.4194,
        "altitude": 15,
        "altitudeAccuracyMeters": null,
        "accuracyMeters": null,
        "headingDegrees": null,
        "speedMetersPerSecond": null
      },
      "deviceModel": "iPhone",
      "devicePlatform": "iOS",
      "inviteeEmail": "person@example.com",
      "thumbnailUrl": "https://…",
      "downloadUrl": "https://…"
    }
  ],
  "pagination": {
    "page": 1,
    "totalPages": 1,
    "total": 1,
    "hasPreviousPage": false,
    "hasNextPage": false
  }
}

Get one photo

The same record as the capture detail page, plus assetUrl for the signed JPEG. The sample keeps the fields you branch on. device also includes screen, camera list, GPU, and input. sensors.geolocation repeats the location object.

GET /api/capture/assets/{assetId}

Example request

curl https://truthscan.com/api/capture/assets/ASSET_ID \
  -H 'x-api-key: YOUR_API_KEY'

Example response

{
  "id": "22222222-2222-4222-8222-222222222222",
  "sessionId": "11111111-1111-4111-8111-111111111111",
  "verificationState": "trusted",
  "trustTier": "standard",
  "risk": { "score": 0.95, "flags": ["geolocation_low_accuracy"] },
  "inviteeEmail": "person@example.com",
  "mimeType": "image/jpeg",
  "assetUrl": "https://…",
  "thumbnailUrl": null,
  "capturedAtUtc": "2026-10-07T15:50:00.000Z",
  "submittedAtUtc": "2026-10-07T15:50:01.200Z",
  "serverReceivedAtUtc": "2026-10-07T15:50:02.000Z",
  "signedAtUtc": "2026-10-07T15:50:03.000Z",
  "createdAtUtc": "2026-10-07T15:50:03.100Z",
  "captureToSubmitMs": 1200,
  "timing": {
    "capturedAtUtc": "2026-10-07T15:50:00.000Z",
    "submittedAtUtc": "2026-10-07T15:50:01.200Z",
    "serverReceivedAtUtc": "2026-10-07T15:50:02.000Z",
    "signedAtUtc": "2026-10-07T15:50:03.000Z",
    "captureToSubmitMs": 1200
  },
  "geolocation": {
    "latitude": 37.7749,
    "longitude": -122.4194,
    "altitude": 15,
    "altitudeAccuracyMeters": 10,
    "accuracyMeters": 12,
    "headingDegrees": null,
    "speedMetersPerSecond": null
  },
  "deviceModel": "iPhone",
  "devicePlatform": "iOS",
  "device": {
    "browserName": "Mobile Safari",
    "platform": "iOS",
    "timezone": "America/Los_Angeles",
    "screenWidth": 390,
    "screenHeight": 844,
    "camera": { "width": 1920, "height": 1080, "facingMode": "environment" },
    "connection": { "effectiveType": "4g", "downlinkMbps": 10, "rttMs": 50 },
    "network": {
      "publicIp": "203.0.113.10",
      "country": "US",
      "city": "San Francisco"
    }
  },
  "sensors": {
    "gyroVariance": 0.02,
    "accelVariance": 0.11,
    "motionSampleCount": 24,
    "permissionDenied": {
      "camera": false,
      "geolocation": false,
      "motion": false
    }
  }
}

Result fields

verificationState, trustTier, and risk.flags are the values to branch on. assetUrl and downloadUrl expire after one hour.

FieldMeaning
verificationStatetrusted, valid_untrusted, invalid, or none
trustTierstandard or high
risk.score0 to 1. Starts at 1. Each sensor flag lowers it.
risk.flagscamera_permission_denied, geolocation_permission_denied, geolocation_missing, geolocation_low_accuracy, motion_permission_denied, motion_samples_missing, gyro_missing, zero_gyro_variance, accel_missing
geolocationlatitude, longitude, altitude, altitudeAccuracyMeters, accuracyMeters, headingDegrees, speedMetersPerSecond
deviceBrowser, platform, user agent, timezone, screen, camera, connection (type, downlink, RTT), and network (public IP, ASN, organization, country, city, region)
sensorsmotionSampleCount, gyroVariance, accelVariance, and permissionDenied for camera, geolocation, and motion
timingShutter, submit, server received, and signed times, plus captureToSubmitMs

Webhook

After a trusted capture, TruthScan POSTs capture.completed to webhookUrl. Redirects are not followed. The body does not include precise location. Fetch the asset for the full record.

Read X-TruthScan-Signature. It is t=<unix seconds>,v1=<hex>. Compute HMAC-SHA256 over t + "." + the raw body using webhookSecret, and reject timestamps more than five minutes off.

Example body

{
  "id": "…",
  "type": "capture.completed",
  "data": {
    "sessionId": "11111111-1111-4111-8111-111111111111",
    "assetId": "22222222-2222-4222-8222-222222222222",
    "verificationState": "trusted",
    "trustTier": "standard",
    "capturedAtUtc": "2026-10-07T15:50:00.000Z"
  }
}

Rate limits

Limits are shared across instances. A 429 includes Retry-After in seconds.

Limits

Each call counts as one request against both the IP and the organization.

PathBucketLimit
Create and readIP600 / minute
Create and readOrganization2,000 / minute
Phone captureIP60 / minute
Phone captureOrganization2,000 / minute

Create and read covers POST /api/capture/session/create plus the three GET result routes. Phone capture covers nonce, upload-url, and sign. The phone does not send your API key.

Throttled response

Wait for Retry-After, then send the request again.

HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json

{
  "error": "Rate limit exceeded",
  "code": "RATE_LIMIT_EXCEEDED",
  "retryAfter": 12
}

What to read

  • Retry-After: seconds until the window resets
  • retryAfter: the same value in the JSON body

Best practices

  • Poll session status instead of listing assets on a tight loop.
  • Prefer the webhook when you can receive HTTPS callbacks.

Errors

Failed calls return JSON with error and code.

401 and 403 mean the key cannot be used. 404 means the session or asset is not in your organization.

CodeMeaning
MISSING_API_KEYNo x-api-key or Bearer token
INVALID_API_KEYKey is unknown, inactive, or has no organization
ORG_NO_API_ACCESSOrganization API access is off
INVITEE_EMAIL_REQUIREDinviteeEmail was missing
INVITEE_EMAIL_INVALIDinviteeEmail is not a valid address
INVITE_MESSAGE_REQUIREDsendEmail is true and message is missing or too long
INVITE_SUBJECT_REQUIREDsendEmail is true and subject is missing
WEBHOOK_URL_REJECTEDwebhookUrl is not public https
SESSION_NOT_FOUNDNo session with that id in your organization
ASSET_NOT_FOUNDNo asset with that id in your organization
RATE_LIMIT_EXCEEDEDToo many requests in the current window

Frequently asked questions

How the Capture API fits into your backend.