# ID.PHOTOS MCP service — instructions for agents

You are reading the reference for an MCP server that turns an ordinary photograph into a
**compliant ID photo** for more than a hundred national standards — passports, visas, residence
permits, driving licences — and returns the compliance verdict with it.

**No account, no sign-up, no API key.** Connect and start working. Rendering, the compliance
checks and the watermarked previews are free; one finished photo is paid for at a time.

Everything on this page is stable and machine-readable. Human overview:
<https://id.photos/retail/mcp/doc>

---

## 1. What this service is for

Give it a photograph of a person and a standard id. It returns a photo cropped, sized, levelled
and measured to that standard, plus a per-rule pass or fail, so you can tell the person what is
wrong before anything is printed or paid for.

It does **not** take photographs, judge whether someone is eligible for a document, or submit
anything to any government.

---

## 2. Connect

```
Endpoint   POST   https://id.photos/retail/mcp/server    (JSON-RPC 2.0, MCP streamable HTTP)
Stream     GET    https://id.photos/retail/mcp/server    (SSE; optional, nothing is pushed)
End        DELETE https://id.photos/retail/mcp/server    (acknowledged; nothing is held)
```

MCP client configuration — no `headers` block, because there is nothing to authenticate as:

```json
{
  "mcpServers": {
    "id-photos": {
      "type": "http",
      "url": "https://id.photos/retail/mcp/server"
    }
  }
}
```

Protocol version `2025-06-18` and the two before it are accepted; `initialize` answers with the
one that was agreed. Single calls and batches both work.

### The `file_id` is the credential

`process_photo` returns a `file_id`. There is no account, so that id is the **only** thing
identifying the work: anyone holding it can read, preview and download that photo.

- **Keep it.** It is how you come back to the session — after a dropped reply, a restart, or a
  day later.
- **Treat it as a secret.** Do not log it where a third party can read it, and do not put it in a
  URL you hand to anyone but the customer it belongs to.
- It is a v4 UUID and is never re-issued.

### Follow `next_step`

Every answer carries a **`next_step`** naming the single call to make next, worked out from the
state that photo is actually in. Follow it. It is the authority when it disagrees with the worked
run below, because it can see the session and the run is only a sketch.

It is ordered by what costs most to get wrong:

| If | `next_step` says |
|---|---|
| The order is paid | `download_photo` |
| A transaction is pending | Wait and re-read with `get_session` — do not pay again |
| A compliance check failed | Fix the photograph, naming what failed — **not** the checkout |
| Both renditions exist, none chosen | `select_rendition`, before the price is fixed |
| Otherwise | `get_purchase_options` |

The two middle rows are the ones that save money. A photo that fails a check is rejected at the
counter and the customer has still paid; and an order paid before a rendition is chosen delivers
the plain one, after which the choice is refused.

---

## 3. What is free and what is paid

| Free | Paid |
|---|---|
| `list_standards`, `get_photo_spec`, `process_photo`, `process_photo_compare`, `select_rendition`, `get_session`, `get_preview`, `get_purchase_options` | `download_photo` |

**You are charged when a photo is delivered, not when it is made.** Render as many times as it
takes to get the framing right; nothing is owed until the finished file is served.

One payment buys one photo — this session, this standard. A second standard for the same person is
a second photo and a second payment. That is correct, not something to work around.

Price comes from `get_purchase_options`, per session. Apple and Google localise and tax-adjust
their own prices, so **what the store shows the customer is what they pay**; the amount in the
answer is a reference.

---

## 4. Tools

Arguments are JSON. Images are base64 strings (a `data:` URL is also accepted), up to **15 MB
decoded**.

### `list_standards` — free
`{ "country": "Canada" }` (optional filter, matched against country and id). Returns every public
standard with its printed size, resolution and description. Use `id` from here as `standard_id`.

### `get_photo_spec` — free
`{ "standard_id": "CA_PP" }`. One standard's specification, plus **`spec_url`** — the published
page on id.photos that sets the rules out for a person to read.

Hand that link over rather than paraphrasing the rules; it is the version that stays current when
a country changes them. You **cannot build the URL yourself** — the slugs are not a mechanical
function of country and purpose. `spec_url_without_web_checkout` is the same page with the
website's own "get my photo" calls to action suppressed, which is the one to use if you are
selling the photo yourself.

The standard id comes back as both `id` and `standard_id`, so either key works.

### `process_photo` — free
```json
{
  "image_base64": "…",
  "standard_id": "CA_PP",
  "filename": "photo.jpg",
  "background_type": "original"
}
```
Returns `file_id`, the per-rule `validation`, the applied measurements, and `next_step`. Nothing
is charged. **Keep the `file_id`.**

- `background_type` — `original` keeps the photograph's own background. A removal provider puts
  the subject on a studio background and is sold as the higher tier.

### `process_photo_compare` — free
Same arguments without `background_type`. Renders **both** renditions — the photo's own background
and the studio one — and keeps them, so the customer can look at the two before choosing. Both
previews are watermarked and free. Choose with `select_rendition`.

### `select_rendition` — free
`{ "file_id": "…", "variant": "original" }` — `original` or `plus`. Sets which rendition the order
delivers, and therefore which tier it is priced at. Refused with `409` once the order is paid:
what was bought cannot change.

### `get_session` — free
`{ "file_id": "…" }`. The authoritative state: compliance checks, chosen rendition,
`payment_status`, and `payment_method` once something has paid.

**Ask, rather than remembering.** A refund or a chargeback can arrive months after a sale, so a
photo that was paid yesterday may not be paid today. This call is the answer; your own cache is not.

### `get_preview` — free
`{ "file_id": "…", "variant": "preview" }` — `preview` or `preview_plus`. Returns the watermarked
preview as `image_base64`. Enough to judge the photo before buying it. It will not serve a paid
file.

### `get_purchase_options` — free
`{ "file_id": "…" }`. The price, the currency, the payment methods, and every payment attempt
recorded against this photo so far.

Read **`how`** on each provider before trying to pay. Each carries `available`, so a method that
exists but is switched off is visible rather than silently absent, and `planned_providers` names
what is coming — do not hard-code today's list.

An amount of `null` means the price could not be worked out. It does not mean free; say you do not
know rather than quoting zero.

### `create_checkout` — free
```json
{ "file_id": "…", "success_url": "…", "cancel_url": "…" }
```
Opens a card payment and returns **`checkout_url`**, a Stripe-hosted page. The two URLs are
optional and default to this site.

This is the one payment method **you can start yourself**. Give the customer the URL and let them
pay on Stripe's page.

> **Never ask anyone for a card number, and never put one in a tool argument.** This service
> cannot accept one and will not. A card number in a tool call is a card number in a transcript
> and in every log it passes through.

There is nothing to redeem afterwards — see below. Poll `get_session` until it reads `PAID`.

### `register_payment_attempt` — free
```json
{ "file_id": "…", "platform": "ios", "transaction_id": "…",
  "product_id": "…", "purchase_token": "…" }
```
Records a store transaction id **as soon as the store issues it**, before the purchase finishes.
`purchase_token` is required on Android: Play identifies a purchase by token, never by order id.

Do this **even when the purchase then fails.** The stores send refunds, reversals and chargebacks
about a transaction for months afterwards, and one that was never announced cannot be matched back
to a photo when that happens.

### `redeem_purchase` — free
```json
{ "file_id": "…", "platform": "android", "product_id": "…",
  "purchase_token": "…" }
```
`transaction_id` on iOS, `purchase_token` on Android. The receipt is verified **with the store that
issued it** and the order is then paid.

Idempotent: re-send the same transaction if you never saw the answer. The same transaction against
a different photo is refused with `409` — one receipt buys one photo.

### `download_photo` — needs a paid order
`{ "file_id": "…", "variant": "full" }` — `full`, `full_printable` for the print sheet, or
`full_plus` for the studio rendition. Returns the finished, unwatermarked `image_base64`.

An unpaid session is refused with `402`. Downloading again, later, is free: the payment is on the
photo, not on the call.

---

## 5. Paying through a store — and why you cannot start one

There are two ways to pay, and they differ in **who can begin the payment**. `get_purchase_options`
says which is which in each provider's `kind`; this section is the store methods, and the next is
the card.

**An agent cannot start an App Store or Play purchase.** The stores require the customer to buy on
their own device, in their own account, through their own store client. There is no server-side
call that makes a purchase, and nothing in this service can create one.

Your part is to report the result:

```
register_payment_attempt   when the store issues a transaction id
redeem_purchase            when the purchase completes — the receipt is verified, then it is paid
```

So the shape of a real flow is: you render and price the photo, the customer buys it in the app on
their phone, the app hands you the receipt, and you redeem it. If you are not running alongside an
app that can do that, say so to the customer and hand them a link — do not loop on
`redeem_purchase` waiting for a purchase nobody made.

More methods are coming — `planned_providers` names them. Each will say in its own `kind`
whether it is one you can begin, so branching on that field keeps working as the list grows.

---

## 6. Paying by card, start to finish

```
1. get_purchase_options { file_id }
      → providers includes { id: "stripe", kind: "hosted_checkout", available: true }
2. create_checkout { file_id }
      → { checkout_url: "https://checkout.stripe.com/c/pay/cs_…",
          transaction_id: "cs_…", amount, currency }
3. give checkout_url to the customer
      they open it and pay on Stripe's page — you never see the card
4. get_session { file_id }   ← poll this
      → payment_status "PAID"
5. download_photo { file_id }
```

**Branch on `kind`, not on the provider's name.** `hosted_checkout` is a method you begin;
`in_app_purchase` is one only the customer's device can begin. A method added later will say
which it is in the same field.

### There is no receipt to hand back

Apple and Google give the customer a signed receipt, and your job is to carry it to
`redeem_purchase`. **Stripe does not.** It issues no credential the customer can pass on, so
there is nothing for you to redeem and no `redeem_purchase` call for a card.

What makes a card order paid is Stripe confirming it to the server directly, over a signed
channel you are not part of. That has three consequences worth planning for:

| | What it means for you |
|---|---|
| You cannot assert payment | Nothing you send can mark an order paid. `get_session` is the only way to find out |
| It can land without you | If the customer pays after your run ends, the order is still paid and the photo still delivered |
| Confirmation is not instant | Usually seconds. Poll `get_session` rather than assuming the checkout URL opening means anything |

If `create_checkout` is refused with *"Card payment is not available on this service"*, the
deployment has no card processing configured. `get_purchase_options` says so in advance —
`available: false` on that provider — so check there rather than discovering it here.

### What it does not change

The photo, the price and the paywall are the same whichever way it is paid. A card order and a
store order both end at one `payment_status`, and a refund takes a card order back exactly as it
takes a store order back: `download_photo` starts refusing again, and `get_session` says why.

---

## 7. A worked run

```
1. list_standards { "country": "Canada" }               -> id "CA_PP"
2. get_photo_spec { "standard_id": "CA_PP" }            -> 50x70 mm @600dpi, spec_url
3. process_photo { image_base64, standard_id: "CA_PP" } -> file_id, validation, next_step
   ├─ a rule failed?   next_step names it; take another photograph and process again
   └─ all passed?      continue
4. get_preview { file_id }                              -> watermarked image, free
5. get_purchase_options { file_id }                     -> price, providers, how
6. the customer buys in the app on their own device
7. register_payment_attempt { file_id, platform, transaction_id }
8. redeem_purchase { file_id, platform, product_id, … } -> payment_status PAID
9. download_photo { file_id }                           -> the finished photo
```

Steps 1 to 5 cost nothing however many times they run.

After `process_photo_compare` instead of `process_photo`, a `select_rendition` step belongs
between 4 and 5 — which is what `next_step` will tell you, without your having to know it.

---

## 8. Errors

A failed tool answers as a **result** with `isError: true` and readable text — not a transport
error — so you can act on it. The text starts with the status where there is one.

| | Meaning | What to do |
|---|---|---|
| `400` | Unknown standard, malformed image, bad argument, or a receipt the store rejected | Read the text; it names the field or the reason |
| `402` | The photo has not been paid for | `get_purchase_options`; the customer buys on their device |
| `404` | No such session, or that rendition was never made | Check the `file_id`; render the rendition you are asking for |
| `409` | Already paid and so cannot change; a receipt already used for another photo; a purchase the store has not settled yet | Re-read with `get_session`. "Not settled yet" is worth retrying shortly |
| `410` | The session has been removed | Start a new one; it cannot be recovered |
| `413` | The photograph is larger than 15 MB decoded | Downscale and send again |
| `503` | A processing service is briefly unavailable | Retry |

A refunded or revoked purchase is reported plainly rather than as a failure of the photo. The
session simply stops being paid, and `get_session` says so.

---

## 9. Rules to follow

- **Keep the `file_id` and treat it as a secret.** It is the whole credential. Losing it loses the
  photo, including one that has been paid for; leaking it gives someone else the paid download.
- **Check the validation results before taking payment.** A photo that fails a rule will be
  rejected at the counter, and the customer has still paid.
- **Never promise a photo you have not downloaded.** The watermarked preview is not the product.
  `next_step` in every answer says which side of the paywall you are on.
- **Do not re-upload to change something.** Re-processing the same photograph starts a **new**
  session with a new `file_id` and a new payment. To change the rendition, use `select_rendition`.
- **Ask about payment; do not assume it.** `get_session` is the authority, every time, because a
  refund can arrive long after the sale.
- **Announce every transaction id, including the ones that fail.** It is what lets a refund months
  later be matched to the right photo.
- **Never send a photograph you were not asked to process.** Every image is a picture of a real
  person.

---

## 10. This is not the business service

There are two MCP services here and they are not interchangeable.

| | This service | `/mcp` |
|---|---|---|
| Who calls | Anyone, anonymously | A reviewed business account |
| Credentials | None | Client id and secret |
| What a photo costs | Paid one at a time, through a store | Credits on the account |
| Scope | One photo session, named by its `file_id` | Every session on the account |

Pointing a client at one URL does not reach the other's tools. If you are building for a business
that buys photos in volume, the business service is the one to ask about — it is a separate
endpoint under `/mcp`, with its own reference.
