Skip to content
TakPhoto

REST API · v1

Compliant passport photos from one API call

Send a photo and the document it is for. It comes back cropped, sized and checked against that document's official rules, with the result of every check — the same engine as our own site, for apps, print shops and kiosks.

Base URL https://api.takiphoto.com/api/v1

API status: Checking…

Quickstart

From nothing to a finished photo in three steps.

  1. Get a test key

    Open an API account and create a test key — it starts with pk_test_. Test keys are free and return a watermarked preview.

    Get a test key
  2. Send a photo

    POST the photo with the document you need. The answer comes back as soon as the photo is ready, or as a 202 with a Location header to poll.

    curl -X POST "https://api.takiphoto.com/api/v1/photos" \
      -H "Authorization: Bearer pk_test_EXAMPLE000000000000000000000000000000000000" \
      -F "[email protected]" \
      -F "spec_id=us-passport" \
      -F 'outputs=["digital"]'
  3. Read the result

    The photo carries its status, every check it was measured against and, once it is completed, signed links to its files. This is its shape:

    {
      "checks": [
        {
          "code": "string",
          "hint": "string",
          "result": "string",
          "value": 0
        }
      ],
      "created_at": "2026-01-01T12:00:00Z",
      "credits_charged": 0,
      "crop": {
        "rotation_deg": 0,
        "scale": 0,
        "x0": 0,
        "x1": 0,
        "y0": 0,
        "y1": 0
      },
      "expires_at": "2026-01-01T12:00:00Z",
      "id": "string",
      "outputs": {
        "digital": {
          "bytes": 0,
          "height": 0,
          "url": "string",
          "width": 0
        },
        "sheets": {
          "key": {
            "jpeg": "string",
            "pdf": "string"
          }
        }
      },
      "review": {
        "reason": "string",
        "status": "string"
      },
      "spec": {
        "id": "string",
        "version": 0
      },
      "status": "processing"
    }

Code examples

The same three calls in four languages. Swap the example key for your own.

Upload a photo file
curl -X POST "https://api.takiphoto.com/api/v1/photos" \
  -H "Authorization: Bearer pk_test_EXAMPLE000000000000000000000000000000000000" \
  -F "[email protected]" \
  -F "spec_id=us-passport" \
  -F 'outputs=["digital"]'
Read a photo back
curl "https://api.takiphoto.com/api/v1/photos/PHOTO_ID" \
  -H "Authorization: Bearer pk_test_EXAMPLE000000000000000000000000000000000000"
Send a photo by its URL
curl -X POST "https://api.takiphoto.com/api/v1/photos" \
  -H "Authorization: Bearer pk_test_EXAMPLE000000000000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"spec_id": "us-passport", "image_url": "https://example.com/photo.jpg", "outputs": ["digital"]}'

API reference

Generated from the published OpenAPI contract, so it lists exactly what the API answers to — nothing more, nothing less.

Every call carries your API key in the Authorization: Bearer pk_test_… header. Live keys (pk_live_) draw on your plan; test keys (pk_test_) are free and watermark the result.

/photos

post/photosAPI key

Create Photo

A photo from file (multipart) or image_url (JSON). Answers 200 with the result when the photo settles within the wait — completed (one credit on a live key), needs_retake or failed (free) — else 202 with Location to poll. An expert review answers 202 in_review until the expert decides.

Request body application/json
backgroundstring | null

#RRGGBB; the document's own colour when absent.

Limits:pattern: ^#[0-9A-Fa-f]{6}$

callback_urlstring | null

A public https URL told when this photo settles.

Limits:maxLength: 512

expert_reviewboolean

Live keys: have an expert check the photo (one review credit).

Default: false

image_urlstring | nullrequired

JSON body only: a public http(s) URL of the photo, fetched by us (≤ 25 MB).

Limits:maxLength: 2048

outputsarray<string>

The files wanted: the digital photo and any print sheets.

One of:digital4x6in10x15cma4

Limits:minItems: 1maxItems: 4

retouchboolean

The automatic light retouch, when it is offered.

Default: false

spec_idstringrequired

A document from GET /specs.

Limits:minLength: 4maxLength: 64

Example
{
  "background": "string",
  "callback_url": "string",
  "expert_review": false,
  "image_url": "string",
  "outputs": [
    "digital"
  ],
  "retouch": false,
  "spec_id": "string"
}
Request body multipart/form-data
backgroundstring | null

#RRGGBB; the document's own colour when absent.

Limits:pattern: ^#[0-9A-Fa-f]{6}$

callback_urlstring | null

A public https URL told when this photo settles.

Limits:maxLength: 512

expert_reviewboolean

Live keys: have an expert check the photo (one review credit).

Default: false

filestring<binary>required

The photo: JPEG, PNG, HEIC or WebP, at most 25 MB.

outputsstring

The files wanted, as the JSON text of an array: ["digital","4x6in"].

Default: ["digital"]

retouchboolean

The automatic light retouch, when it is offered.

Default: false

spec_idstringrequired

A document from GET /specs.

Limits:minLength: 4maxLength: 64

Responses
    • 200Successful Response
    • 202Still processing after `api.sync_wait_s` (15 s): follow `Location`.
    Example
    {
      "checks": [
        {
          "code": "string",
          "hint": "string",
          "result": "string",
          "value": 0
        }
      ],
      "created_at": "2026-01-01T12:00:00Z",
      "credits_charged": 0,
      "crop": {
        "rotation_deg": 0,
        "scale": 0,
        "x0": 0,
        "x1": 0,
        "y0": 0,
        "y1": 0
      },
      "expires_at": "2026-01-01T12:00:00Z",
      "id": "string",
      "outputs": {
        "digital": {
          "bytes": 0,
          "height": 0,
          "url": "string",
          "width": 0
        },
        "sheets": {
          "key": {
            "jpeg": "string",
            "pdf": "string"
          }
        }
      },
      "review": {
        "reason": "string",
        "status": "string"
      },
      "spec": {
        "id": "string",
        "version": 0
      },
      "status": "processing"
    }
    • 400Invalid request
    • 401Authentication required
    • 402Quota used up
    • 403Not allowed
    • 404Not found
    • 413Upload too large
    • 422Request is not valid
    • 429Rate limited
    • 503Maintenance
    Example
    {
      "code": "string",
      "hint": "string",
      "message": "string"
    }

/photos/{photo_id}

get/photos/{photo_id}API key

Get Photo

The photo as it is now. Under the key's delete_after_download, the first read of a finished photo leaves it one more hour.

Parameters
photo_idstringin the pathrequired
Responses
    • 200Successful Response
    Example
    {
      "checks": [
        {
          "code": "string",
          "hint": "string",
          "result": "string",
          "value": 0
        }
      ],
      "created_at": "2026-01-01T12:00:00Z",
      "credits_charged": 0,
      "crop": {
        "rotation_deg": 0,
        "scale": 0,
        "x0": 0,
        "x1": 0,
        "y0": 0,
        "y1": 0
      },
      "expires_at": "2026-01-01T12:00:00Z",
      "id": "string",
      "outputs": {
        "digital": {
          "bytes": 0,
          "height": 0,
          "url": "string",
          "width": 0
        },
        "sheets": {
          "key": {
            "jpeg": "string",
            "pdf": "string"
          }
        }
      },
      "review": {
        "reason": "string",
        "status": "string"
      },
      "spec": {
        "id": "string",
        "version": 0
      },
      "status": "processing"
    }
    • 400Invalid request
    • 401Authentication required
    • 402Quota used up
    • 403Not allowed
    • 404Not found
    • 413Upload too large
    • 422Request is not valid
    • 429Rate limited
    • 503Maintenance
    Example
    {
      "code": "string",
      "hint": "string",
      "message": "string"
    }
delete/photos/{photo_id}API key

Delete Photo

The photo and every file made from it, at once.

Parameters
photo_idstringin the pathrequired
Responses
    • 204Successful Response
    • 400Invalid request
    • 401Authentication required
    • 402Quota used up
    • 403Not allowed
    • 404Not found
    • 413Upload too large
    • 422Request is not valid
    • 429Rate limited
    • 503Maintenance
    Example
    {
      "code": "string",
      "hint": "string",
      "message": "string"
    }

/photos/{photo_id}/adjust

post/photos/{photo_id}/adjustAPI key

Adjust Photo

Scale and shift the crop (the same rules as the web editor); the photo's files are made again at once. Free: credits_charged does not change.

Parameters
photo_idstringin the pathrequired
Request body application/json
dxnumberrequired

Limits:minimum: -300maximum: 300

dynumberrequired

Limits:minimum: -300maximum: 300

scalenumberrequired

Limits:minimum: 0.7maximum: 1.4

Example
{
  "dx": 0,
  "dy": 0,
  "scale": 0.7
}
Responses
    • 200Successful Response
    Example
    {
      "checks": [
        {
          "code": "string",
          "hint": "string",
          "result": "string",
          "value": 0
        }
      ],
      "created_at": "2026-01-01T12:00:00Z",
      "credits_charged": 0,
      "crop": {
        "rotation_deg": 0,
        "scale": 0,
        "x0": 0,
        "x1": 0,
        "y0": 0,
        "y1": 0
      },
      "expires_at": "2026-01-01T12:00:00Z",
      "id": "string",
      "outputs": {
        "digital": {
          "bytes": 0,
          "height": 0,
          "url": "string",
          "width": 0
        },
        "sheets": {
          "key": {
            "jpeg": "string",
            "pdf": "string"
          }
        }
      },
      "review": {
        "reason": "string",
        "status": "string"
      },
      "spec": {
        "id": "string",
        "version": 0
      },
      "status": "processing"
    }
    • 400Invalid request
    • 401Authentication required
    • 402Quota used up
    • 403Not allowed
    • 404Not found
    • 413Upload too large
    • 422Request is not valid
    • 429Rate limited
    • 503Maintenance
    Example
    {
      "code": "string",
      "hint": "string",
      "message": "string"
    }

/specs

get/specsAPI key

List Specs

The document specs a photo can be made for, verified ones first; country is the ISO 3166-1 alpha-2 code, q matches the id or a name in any language.

Parameters
countrystring | nullin the query

Limits:minLength: 2maxLength: 2

qstring | nullin the query

Limits:maxLength: 64

limitintegerin the query

Default: 50

Limits:minimum: 1maximum: 200

Responses
    • 200Successful Response
    Example
    [
      {
        "background": "string",
        "category": "string",
        "country": "string",
        "id": "string",
        "names": {
          "key": "string"
        },
        "output_px": [
          0
        ],
        "tier": "string",
        "version": 0
      }
    ]
    • 400Invalid request
    • 401Authentication required
    • 402Quota used up
    • 403Not allowed
    • 404Not found
    • 413Upload too large
    • 422Request is not valid
    • 429Rate limited
    • 503Maintenance
    Example
    {
      "code": "string",
      "hint": "string",
      "message": "string"
    }

/specs/{spec_id}

get/specs/{spec_id}API key

Get Spec

Parameters
spec_idstringin the pathrequired
Responses
    • 200Successful Response
    Example
    {
      "allowed_backgrounds": [
        "string"
      ],
      "background": "string",
      "category": "string",
      "country": "string",
      "digital": {
        "format": "string",
        "height_px": 0,
        "max_kb": 0,
        "max_px": 0,
        "min_px": 0,
        "width_px": 0
      },
      "dpi": 0,
      "eye_line_from_bottom_mm": {
        "max": 0,
        "min": 0
      },
      "head_height_mm": {
        "max": 0,
        "min": 0
      },
      "id": "string",
      "names": {
        "key": "string"
      },
      "output_px": [
        0
      ],
      "print_sheets": [
        "string"
      ],
      "rules": {
        "color": "string",
        "expression": "string",
        "glasses": "string",
        "head_covering": "string"
      },
      "size_mm": {
        "height": 0,
        "width": 0
      },
      "source_url": "string",
      "tier": "string",
      "verified_at": "2026-01-01",
      "version": 0
    }
    • 400Invalid request
    • 401Authentication required
    • 402Quota used up
    • 403Not allowed
    • 404Not found
    • 413Upload too large
    • 422Request is not valid
    • 429Rate limited
    • 503Maintenance
    Example
    {
      "code": "string",
      "hint": "string",
      "message": "string"
    }

/usage

get/usageAPI key

Usage

Credits left this period, the key's rate limit and mode, and its calls today.

Responses
    • 200Successful Response
    Example
    {
      "mode": "live",
      "period_end": "2026-01-01T12:00:00Z",
      "photos": {
        "balance": 0,
        "quota": 0,
        "used_this_period": 0
      },
      "rate_limit_per_min": 0,
      "requests_today": 0,
      "reviews": {
        "balance": 0,
        "quota": 0,
        "used_this_period": 0
      }
    }
    • 400Invalid request
    • 401Authentication required
    • 402Quota used up
    • 403Not allowed
    • 404Not found
    • 413Upload too large
    • 422Request is not valid
    • 429Rate limited
    • 503Maintenance
    Example
    {
      "code": "string",
      "hint": "string",
      "message": "string"
    }

Check and error codes

Every code the API can return, from the same list our own apps translate.

Check codes

Each entry in a photo's checks names one of these codes with its result.

CodeWhat it means
file_invalid

We couldn't read that file

Upload a JPEG, PNG or HEIC photo.

file_too_large

File is too large

Export a smaller copy and try again.

image_too_large

Image has too many pixels

Anything up to 50 megapixels works.

no_face

We couldn't find a face

Face the camera in even light with nothing covering your face.

multiple_faces

More than one person

Only you should be in the frame.

head_turned

Head turned to the side

Look straight at the camera.

head_tilted

Head is tilted

Keep your head level and look straight ahead.

eyes_closed

Eyes look closed

Open both eyes and look at the camera.

mouth_open

Mouth is open

Close your mouth with a neutral expression.

not_neutral

Expression isn't neutral

Relax your face; no smile.

blurry

Photo is blurry

Hold still and make sure your face is in focus.

too_dark

Photo is too dark

Face a window or add light in front of you.

too_bright

Photo is overexposed

Move away from direct light.

uneven_lighting

Shadow on one side of the face

Light should come from in front of you, not from the side.

not_color

Colour photo required

Take the photo in colour mode.

busy_background

Busy background

A plain wall behind you gives cleaner edges — or add an expert review.

head_not_measurable

We couldn't measure your head

Face the camera in good light and try again.

low_resolution

Face is too small in the photo

Move closer or use a larger image.

crown_cut

Top of the head is cut off

Leave space above the hair.

shoulders_cut

Frame ends too close to the chin

Include your shoulders.

subject_cut_side

Cut off by the edge

Centre yourself with space on both sides.

eye_line_out_of_range

Eye line out of range

The photo was framed by head height instead.

head_size_out_of_range

Head size out of range for this document

Zoom in or out until the head fits the guide.

file_size_over_limit

File is larger than this document allows

We reduced quality as far as acceptable.

retouch_skipped

Nothing was safe to retouch

The original was kept.

Error codes

A request that fails answers with its HTTP status and an error envelope whose code is one of these.

CodeHTTP statusWhat it means
background_not_allowed400That background colour isn't allowed for this document.
credit_packs_disabled400Credit packs aren't on sale right now.
crop_out_of_bounds400That crop goes outside the photo.
email_required400An email address is required for delivery.
invite_invalid400This invitation isn't valid.
magic_link_invalid400This sign-in link is invalid or has expired — request a new one.
plan_not_available400That plan isn't available.
product_not_available400This product isn't available.
setting_out_of_range400That value is outside the allowed range.
setting_unknown400That setting isn't recognised.
totp_invalid400That verification code isn't valid.
turnstile_failed400The anti-bot check failed — reload the page and try again.
webhook_invalid400The webhook signature or payload is invalid.
api_key_invalid401That API key isn't valid.
api_key_revoked401That API key was revoked.
session_expired401Your session has expired — please sign in again.
unauthorized401Please sign in to continue.
payment_required402This order hasn't been paid yet.
quota_exceeded402The organisation's photo quota is used up — upgrade the plan or buy a credit pack.
review_quota_exceeded402There are no expert reviews left in this billing period.
seat_limit402Your plan has no free seats left — upgrade it or remove a member.
subscription_required402This needs an active subscription — choose a plan in billing.
api_disabled403The API isn't turned on for this organisation.
forbidden403You don't have access to this.
org_forbidden403Your role in this organisation doesn't allow that.
org_suspended403This organisation is suspended — contact support.
totp_required403A second factor is required for this action.
client_not_found404We couldn't find that client.
handoff_not_found404This link has expired — show a new QR code and scan it again.
intake_disabled404This intake link isn't active.
not_found404We couldn't find that.
org_not_found404We couldn't find that organisation.
photo_not_found404We couldn't find that photo.
spec_not_found404We don't recognise that document.
upload_not_found404That upload wasn't found, or it's expired.
already_paid409This order is already paid.
attempts_exhausted409You've reached the retry limit — a reviewer will look at this order.
invalid_state409This isn't possible in the order's current state.
last_owner409An organisation must keep at least one owner.
org_slug_taken409That link name is already taken.
owner_of_organisation409You're the only owner of an organisation. Make another member an owner, or delete the organisation, before deleting your account.
product_has_sales409A product with sales can't be deleted — deactivate it instead.
spec_switch_not_allowed409The document can be changed once, to one of equal or lower price.
upload_already_used409This upload was already turned into an order.
invite_expired410This invitation has expired — ask the owner to send a new one.
export_too_large413That export is over 2 GB — choose a shorter date range.
image_url_invalid422The image address must be a public http(s) link.
image_url_unreachable422We couldn't fetch the image from that address.
logo_invalid422The logo must be a PNG or JPEG of up to 1 MB.
org_slug_reserved422That link name can't be used.
validation_error422That request isn't valid.
webhook_url_invalid422The webhook address must be a public https link.
rate_limited429Too many requests — please wait a moment.
test_key_daily_limit429The test key's daily limit is reached — use a live key for more.
internal_error500Something went wrong on our side.
maintenance503TakPhoto is briefly under maintenance — please try again in a few minutes.

Webhooks

Set a webhook URL on your key, or a callback_url on a single photo, and we send you the photo the moment it settles — no polling.

Events

photo.completed
The photo is finished and its files are ready.
photo.failed
The photo could not be made. Nothing was charged.
review.approved
An expert reviewer approved the photo.
review.rejected
An expert reviewer rejected the photo; the reason is in review.reason.

Headers

X-Event
The event's name.
X-Delivery-Id
This delivery's id.
X-Signature
The signature, once your key has a webhook secret.

Retries

Answer with any 2xx status within 10 seconds. Anything else — another status, a redirect or no answer in time — is a failed attempt, and the delivery is tried again:

  1. Attempt 1: right away
  2. Attempt 2: 1분 after the one before
  3. Attempt 3: 5분 after the one before
  4. Attempt 4: 30분 after the one before
  5. Attempt 5: 2시간 after the one before

If attempt 5 fails too, the delivery is given up.

Payload

A POST with a JSON body: the event, when it happened and the photo exactly as reading it through the API returns it. The same event can arrive more than once, so deduplicate on its id.

{
  "id": "evt_00000000000000000000000000000000",
  "event": "photo.completed",
  "created_at": "2026-01-01T12:00:00Z",
  "data": {
    "checks": [
      {
        "code": "string",
        "hint": "string",
        "result": "string",
        "value": 0
      }
    ],
    "created_at": "2026-01-01T12:00:00Z",
    "credits_charged": 0,
    "crop": {
      "rotation_deg": 0,
      "scale": 0,
      "x0": 0,
      "x1": 0,
      "y0": 0,
      "y1": 0
    },
    "expires_at": "2026-01-01T12:00:00Z",
    "id": "string",
    "outputs": {
      "digital": {
        "bytes": 0,
        "height": 0,
        "url": "string",
        "width": 0
      },
      "sheets": {
        "key": {
          "jpeg": "string",
          "pdf": "string"
        }
      }
    },
    "review": {
      "reason": "string",
      "status": "string"
    },
    "spec": {
      "id": "string",
      "version": 0
    },
    "status": "processing"
  }
}

Verify the signature

X-Signature looks like t=<timestamp>,v1=<hex>. Compute HMAC-SHA256 with your webhook secret over the timestamp, a dot and the raw request body, compare it with v1 in constant time, and reject the delivery when they differ or when the timestamp is more than 300 seconds from your clock.

import hashlib
import hmac
import time

TOLERANCE_S = 300


def verify(secret: str, header: str, raw_body: bytes) -> bool:
    """header is X-Signature; raw_body is the request body before any JSON parsing."""
    parts = dict(item.split("=", 1) for item in header.split(",") if "=" in item)
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or not v1 or abs(time.time() - int(t)) > TOLERANCE_S:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

Pricing

Monthly plans for live keys. Test keys are always free.

Changelog

Every change to /api/v1. A breaking change only ever arrives as a new version, and the old one keeps working for 12 months.

This page has not been translated into this language yet, so it is shown in English. The English version is the reference.

v1 — 2026-09

First release of /api/v1.

Endpoints

  • GET /specs and GET /specs/{spec_id} — the document specification catalogue.
  • POST /photos — upload a photo (multipart, or a guarded image_url); a synchronous answer within 15 seconds, or a 202 with a Location to poll for the result.
  • GET /photos/{photo_id} — a photo's status and outputs.
  • POST /photos/{photo_id}/adjust — free crop/zoom adjustment on a finished result.
  • DELETE /photos/{photo_id} — immediate deletion of the photo and its files.
  • GET /usage — remaining quota and current consumption.
  • GET /content/legal/{name} and GET /content/changelog/{name} — the legal documents and this changelog, as Markdown.

Authentication

Every authenticated call needs Authorization: Bearer <your key>. pk_live_... keys are metered against your quota and produce real files; pk_test_... keys are entirely free and produce a watermarked preview, for testing your integration before going live.

Webhooks

If you set a webhook_url on your key (or a callback_url on a single photo), you receive photo.completed, photo.failed, review.approved and review.rejected events, signed with HMAC-SHA256 in the X-Signature header, with automatic retries up to 5 times on delivery failure.

Limits

Every authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and X-Quota-Remaining. Exceeding your monthly plan quota either stops the service or bills the overage, depending on your plan's setting.

Build the whole integration for free

Test keys cost nothing and return a watermarked preview, so everything works before the first paid photo.

Get a test key