SVOYAGER ID Photo API
Turn any photo into a compliant ID / passport / visa photo.
Overview
What it does
The SVOYAGER ID Photo API turns an ordinary portrait into a compliant document photo. ID Photo is a single product: passport, visa, residence-permit and national-ID are represented as document specifications, not separate products. You upload a photo, generate a watermarked preview against a specification, then finalize to get the no-watermark result.
Preview → final workflow
- 1Upload a photo. Create an upload target and PUT the image bytes to it.
- 2Create a session. Render a watermarked preview against a spec. Review the returned issues.
- 3Finalize. Turn the reviewed session into an order and receive the no-watermark final.
Quickstart
Every request is authenticated with a Bearer secret key. Start in test mode:
# List specifications to find a spec_id
curl -G "https://partner.svoyager.com/v1/photo-specifications" \
-H "Authorization: Bearer svoy_sk_test_YOUR_KEY" \
--data-urlencode "country=US"Base URL & environments
All endpoints are served under a single base URL:
https://partner.svoyager.com/v1There is no environment path or parameter. Whether a request runs in test or live is determined entirely by your API key prefix (svoy_sk_test_… vs svoy_sk_live_…). Responses echo this as livemode.
Authentication
API keys
Authenticate every request with an Authorization header carrying your secret key. Keys are created and rotated in the Partner Center.
Authorization: Bearer svoy_sk_test_R2p9Kx7YOURKEYHERExxxxxxxThe prefix selects the environment: svoy_sk_test_… for test, svoy_sk_live_… for live. A test key can never act on live data (and vice versa) — mismatches return 401 wrong_environment.
Scopes
Keys carry explicit least-privilege scopes. A request missing the required scope returns 403 insufficient_scope.
| Scope | Grants |
|---|---|
| photo.specs.read | List and retrieve photo specifications. |
| photo.sessions.read | Retrieve sessions and their previews. |
| photo.sessions.write | Create uploads and photo sessions (previews). |
| photo.finalize.write | Finalize a session into a no-watermark order. |
| photo.orders.read | Retrieve orders and download finals. |
Keeping keys safe
Secret keys are server-to-server only. Never embed a secret key in browser or mobile code, a public repository, or a client-side bundle. If a key is exposed, rotate it immediately in the Partner Center.
Endpoints
Required fields are marked with *. Examples use placeholder credentials — never real keys.
/v1/uploadsUpload a photo
Create a short-lived presigned upload target, then PUT the raw image bytes straight to `upload_url`. This bypasses request-body size limits (product cap 10 MB). No quota is spent.
photo.sessions.write| Field | Type | Description |
|---|---|---|
| content_type* | string | MIME type of the image.image/jpegimage/pngimage/webpimage/heic |
- `upload_url` is single-use and expires; upload promptly and do not modify the URL.
# 1) Create the upload target
curl -X POST "https://partner.svoyager.com/v1/uploads" \
-H "Authorization: Bearer svoy_sk_test_R2p9Kx7YOURKEYHERExxxxxxx" \
-H "Content-Type: application/json" \
-d '{"content_type":"image/jpeg"}'
# 2) PUT the bytes to the returned upload_url (exactly as returned)
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: image/jpeg" \
--data-binary @photo.jpg{
"object": "upload",
"upload_url": "https://upload.svoyager.com/v1/EXAMPLE?signature=…",
"upload_ref": "up_ref_tXk3W2b8Qe7Ra1Nc0",
"max_bytes": 10485760,
"expires_at": "2026-07-24T18: 20: 00Z"
}| Status | Code | When |
|---|---|---|
| 400 | missing_content_type | `content_type` was not provided. |
| 415 | unsupported_format | The content type is not an accepted image format. |
| 503 | temporarily_unavailable | Uploads are temporarily unavailable. |
| 502 | upload_init_failed | The upload could not be initialised — retry shortly. |
Plus the standard authentication, permission and rate-limit errors — see Errors & retries.
/v1/photo-sessionsCreate a photo session
Generate a watermarked preview from an upload against a specification. Spends one `preview` unit of your plan. Review `issues` before finalizing.
photo.sessions.write Idempotency-Key required| Field | Type | Description |
|---|---|---|
| upload_ref* | string | The `upload_ref` from Upload a photo. |
| spec_id* | string | The `photo_specification` id (`spc_…`) to render against. |
curl -X POST "https://partner.svoyager.com/v1/photo-sessions" \
-H "Authorization: Bearer svoy_sk_test_R2p9Kx7YOURKEYHERExxxxxxx" \
-H "Idempotency-Key: a7f3c9e1-2b4d-4c8a-9f10-3e5d6b7a8c9d" \
-H "Content-Type: application/json" \
-d '{"upload_ref":"up_ref_tXk3W2b8Qe7Ra1Nc0","spec_id":"spc_9wTv2c1Kd8Qe"}'{
"object": "photo_session",
"id": "3f9c1e77-2b8a-4a1e-9d2c-6b5a4c3d2e10",
"livemode": false,
"status": "preview_ready",
"spec_id": "spc_9wTv2c1Kd8Qe",
"preview_url": "https://partner.svoyager.com/v1/photo-sessions/3f9c1e77-2b8a-4a1e-9d2c-6b5a4c3d2e10/preview",
"watermarked": true,
"issues": [],
"created_at": "2026-07-24T18: 30: 00Z"
}| Status | Code | When |
|---|---|---|
| 400 | missing_idempotency_key | The `Idempotency-Key` header is missing. |
| 400 | missing_upload_ref | `upload_ref` is missing. |
| 400 | missing_spec_id | `spec_id` is missing. |
| 400 | invalid_upload_ref | The referenced upload could not be read. |
| 413 | file_too_large | The uploaded file exceeds 10 MB. |
| 415 | unsupported_format | The uploaded file is not an accepted image format. |
| 404 | spec_not_found | No active specification matches `spec_id`. |
| 402 | no_active_plan | No active plan is assigned for this environment. |
| 402 | quota_exceeded | Included preview quota for the period is exhausted. |
| 402 | plan_inactive | The plan is not active for this environment. |
| 409 | idempotency_key_reuse | The key was reused with a different body. |
| 409 | request_in_progress | A request with this key is still processing. |
| 422 | unprocessable_image | The image could not be processed. |
| 502 | provider_error | The preview could not be generated upstream — retry shortly. |
| 503 | provider_busy | The processing service is busy — retry shortly. |
Plus the standard authentication, permission and rate-limit errors — see Errors & retries.
/v1/photo-sessions/{id}Retrieve a photo session
Fetch a session by id, including its current status, `preview_url`, and any `issues`.
photo.sessions.read| Field | Type | Description |
|---|---|---|
| id* | string | Session id. |
curl "https://partner.svoyager.com/v1/photo-sessions/3f9c1e77-2b8a-4a1e-9d2c-6b5a4c3d2e10" \
-H "Authorization: Bearer svoy_sk_test_R2p9Kx7YOURKEYHERExxxxxxx"{
"object": "photo_session",
"id": "3f9c1e77-2b8a-4a1e-9d2c-6b5a4c3d2e10",
"livemode": false,
"status": "preview_ready",
"spec_id": "spc_9wTv2c1Kd8Qe",
"preview_url": "https://partner.svoyager.com/v1/photo-sessions/3f9c1e77-2b8a-4a1e-9d2c-6b5a4c3d2e10/preview",
"watermarked": true,
"issues": [],
"created_at": "2026-07-24T18: 30: 00Z"
}| Status | Code | When |
|---|---|---|
| 404 | session_not_found | No session matches the id for this account/environment. |
Plus the standard authentication, permission and rate-limit errors — see Errors & retries.
/v1/photo-sessions/{id}/previewDownload the preview image
Stream the watermarked preview image bytes for a session (SVOYAGER-hosted; the upstream URL is never exposed). This is the resource `preview_url` points to.
photo.sessions.readReturns image bytes| Field | Type | Description |
|---|---|---|
| id* | string | Session id. |
- Returns raw `image/*` bytes on success (not JSON).
curl "https://partner.svoyager.com/v1/photo-sessions/3f9c1e77-2b8a-4a1e-9d2c-6b5a4c3d2e10/preview" \
-H "Authorization: Bearer svoy_sk_test_R2p9Kx7YOURKEYHERExxxxxxx" \
--output preview.jpgOn success the endpoint streams raw image/* bytes (not JSON). Save the response body to a file.
| Status | Code | When |
|---|---|---|
| 404 | preview_not_found | The session or its preview does not exist. |
Plus the standard authentication, permission and rate-limit errors — see Errors & retries.
/v1/photo-sessions/{id}/finalizeFinalize a photo
Turn a reviewed session into a deliverable order. In live mode this produces the no-watermark final and spends one `final` unit; in test mode it emulates the final from the preview.
photo.finalize.write Idempotency-Key required| Field | Type | Description |
|---|---|---|
| id* | string | Session id to finalize. |
curl -X POST "https://partner.svoyager.com/v1/photo-sessions/3f9c1e77-2b8a-4a1e-9d2c-6b5a4c3d2e10/finalize" \
-H "Authorization: Bearer svoy_sk_live_A8m4Qz1YOURKEYHERExxxxxxx" \
-H "Idempotency-Key: a7f3c9e1-2b4d-4c8a-9f10-3e5d6b7a8c9d"{
"object": "photo_order",
"id": "8a2d5b40-71c6-4f0e-b3a9-1e7c9d05f284",
"livemode": true,
"sandbox_emulated": false,
"status": "final_ready",
"session_id": "3f9c1e77-2b8a-4a1e-9d2c-6b5a4c3d2e10",
"spec_id": "spc_9wTv2c1Kd8Qe",
"final_url": "https://partner.svoyager.com/v1/orders/8a2d5b40-71c6-4f0e-b3a9-1e7c9d05f284/final",
"watermarked": false,
"created_at": "2026-07-24T18: 31: 00Z"
}| Status | Code | When |
|---|---|---|
| 400 | missing_idempotency_key | The `Idempotency-Key` header is missing. |
| 404 | session_not_found | No session matches the id for this account/environment. |
| 422 | session_not_finalizable | The session is not in a finalizable state. |
| 402 | no_active_plan | No active plan is assigned for this environment. |
| 402 | quota_exceeded | Included final quota for the period is exhausted. |
| 402 | plan_inactive | The plan is not active for this environment. |
| 409 | idempotency_key_reuse | The key was reused with a different request. |
| 409 | request_in_progress | A request with this key is still processing. |
| 502 | provider_error | The final could not be produced upstream — retry shortly. |
| 503 | provider_busy | The processing service is busy — retry shortly. |
Plus the standard authentication, permission and rate-limit errors — see Errors & retries.
/v1/orders/{id}Retrieve an order
Fetch a finalized order by id, including its status and `final_url` once ready.
photo.orders.read| Field | Type | Description |
|---|---|---|
| id* | string | Order id. |
curl "https://partner.svoyager.com/v1/orders/8a2d5b40-71c6-4f0e-b3a9-1e7c9d05f284" \
-H "Authorization: Bearer svoy_sk_live_A8m4Qz1YOURKEYHERExxxxxxx"{
"object": "photo_order",
"id": "8a2d5b40-71c6-4f0e-b3a9-1e7c9d05f284",
"livemode": true,
"sandbox_emulated": false,
"status": "final_ready",
"session_id": "3f9c1e77-2b8a-4a1e-9d2c-6b5a4c3d2e10",
"spec_id": "spc_9wTv2c1Kd8Qe",
"final_url": "https://partner.svoyager.com/v1/orders/8a2d5b40-71c6-4f0e-b3a9-1e7c9d05f284/final",
"watermarked": false,
"created_at": "2026-07-24T18: 31: 00Z"
}| Status | Code | When |
|---|---|---|
| 404 | order_not_found | No order matches the id for this account/environment. |
Plus the standard authentication, permission and rate-limit errors — see Errors & retries.
/v1/orders/{id}/finalDownload the final image
Stream the final image bytes for an order (SVOYAGER-hosted). In live mode this is the no-watermark deliverable; in test mode it is the sandbox-emulated copy. This is the resource `final_url` points to.
photo.orders.readReturns image bytes| Field | Type | Description |
|---|---|---|
| id* | string | Order id. |
- Returns raw `image/*` bytes on success (not JSON).
curl "https://partner.svoyager.com/v1/orders/8a2d5b40-71c6-4f0e-b3a9-1e7c9d05f284/final" \
-H "Authorization: Bearer svoy_sk_live_A8m4Qz1YOURKEYHERExxxxxxx" \
--output final.jpgOn success the endpoint streams raw image/* bytes (not JSON). Save the response body to a file.
| Status | Code | When |
|---|---|---|
| 404 | final_not_found | The order or its final does not exist. |
Plus the standard authentication, permission and rate-limit errors — see Errors & retries.
/v1/photo-specificationsList photo specifications
Return a cursor-paginated list of available photo specifications. Filter by country or document type to find the `spec_id` you need.
photo.specs.read| Field | Type | Description |
|---|---|---|
| limit | integer | Page size, 1–100 (default 20). |
| starting_after | string | A specification id (`spc_…`) to page after. |
| country | string | ISO-3166 alpha-2 filter, e.g. `US`. |
| document_type | string | Filter by document type, e.g. `passport`. |
curl -G "https://partner.svoyager.com/v1/photo-specifications" \
-H "Authorization: Bearer svoy_sk_test_R2p9Kx7YOURKEYHERExxxxxxx" \
--data-urlencode "country=US" \
--data-urlencode "limit=20"{
"object": "list",
"data": [
{
"object": "photo_specification",
"id": "spc_9wTv2c1Kd8Qe",
"name": "United States Passport"
}
],
"has_more": false,
"next_cursor": null
}This endpoint returns only the standard errors below.
Plus the standard authentication, permission and rate-limit errors — see Errors & retries.
/v1/photo-specifications/{id}Retrieve a photo specification
Fetch a single specification by its stable `spec_id`, including its full requirements.
photo.specs.read| Field | Type | Description |
|---|---|---|
| id* | string | Specification id (`spc_…`). |
curl "https://partner.svoyager.com/v1/photo-specifications/spc_9wTv2c1Kd8Qe" \
-H "Authorization: Bearer svoy_sk_test_R2p9Kx7YOURKEYHERExxxxxxx"{
"object": "photo_specification",
"id": "spc_9wTv2c1Kd8Qe",
"name": "United States Passport",
"country_code": "US",
"document_type": "passport",
"physical_size": {
"width": 51,
"height": 51,
"unit": "mm"
},
"pixel_size": {
"width": 600,
"height": 600
},
"dpi": 300,
"background": {
"color": "#FFFFFF",
"name": "white"
},
"requirements": {
"head_height": "25–35 mm (1–1⅜ in)",
"eye_to_bottom": "28–35 mm (1⅛–1⅜ in)",
"recency_months": 6,
"expression": "neutral",
"color": true,
"glasses": "not allowed",
"eyes_and_ears": "both eyes open"
},
"updated_at": "2026-07-01T12: 00: 00Z"
}| Status | Code | When |
|---|---|---|
| 404 | spec_not_found | No active specification matches the id. |
Plus the standard authentication, permission and rate-limit errors — see Errors & retries.
Send an Idempotency-Key header (a unique value, e.g. a UUID) so a retried request never double-charges or double-processes. It is required on:
POST /v1/photo-sessionsPOST /v1/photo-sessions/{id}/finalize
- • Same key + same body → the original result is replayed (no second charge, no second render).
- • Same key + a different body →
409 idempotency_key_reuse. - • A retryable failure lets you retry with the same key, body and
upload_ref. - • Concurrent finalize requests for one session can never create or charge two orders.
A session moves through preview states; an order through finalization states.
preview_ready— preview generated, no issues.preview_has_issues— preview generated with issues.preview_failed— the preview could not be produced.finalized— the session was finalized.
final_ready— the final is available atfinal_url.processing— the final is being prepared.failed— finalization failed.
The environment is decided by your key prefix — clients can never pass it. Test mode runs the real, watermarked preview so you exercise the true flow, but emulates the final so no live quota or production render is used.
| Aspect | Test | Live |
|---|---|---|
| livemode | false | true |
| Preview | Real, watermarked | Real, watermarked |
| Final | Sandbox-emulated from the preview | Real, no watermark |
| sandbox_emulated | true (on the order) | false |
| watermarked (final) | true | false |
| Quota consumed | Test quota only | Live quota only |
Your plan includes a number of previews and finals per period, metered separately. Creating a session spends one preview; finalizing spends one final. Quota is atomically reserved before processing.
- When the included quota for a meter is exhausted, the request returns
402 quota_exceeded. 402is a hard stop — retrying will not succeed until the next period or a plan change.- Test and live quotas are fully independent.
Every error uses one envelope. The request_id also appears in the X-Request-Id response header — include it in support requests.
{
"error": {
"type": "invalid_request_error",
"code": "quota_exceeded",
"message": "Your included final quota for this period is exhausted.",
"request_id": "req_9wTv2c1Kd8QeR7"
}
}Standard errors (all endpoints)
| Status | Code | When |
|---|---|---|
| 401 | invalid_api_key | Missing, malformed, or unknown API key. |
| 401 | revoked_api_key | The key was revoked. |
| 401 | expired_api_key | The key passed its rotation grace period. |
| 401 | wrong_environment | A test key was used on live traffic (or vice versa). |
| 403 | client_disabled | The API client is disabled. |
| 403 | api_access_revoked | API access for the account is not active. |
| 403 | insufficient_scope | The key lacks the required scope for this endpoint. |
| 429 | rate_limited | Per-key rate limit exceeded. Honour `Retry-After`. |
422 is returned only for a confirmed image problem. 402 means quota is exhausted. 409 is an idempotency or resource conflict.
429 means you hit the per-key rate limit — honour the Retry-After header. For 502/503 and other 5xx, retry with backoff and the same Idempotency-Key.
A preview can return zero or more issues, each a stable { code, message }. Surface these to the end user so they can retake the photo. An unrecognised upstream signal degrades to other.
| Code | Message |
|---|---|
| face_not_found | No face was detected in the photo. |
| uneven_lighting | Lighting on the face is uneven. |
| poor_brightness | Face brightness is too dark, too bright, or the photo is low quality. |
| low_sharpness | The photo is blurry or not sharp enough. |
| eyeglasses_not_allowed | Eyeglasses are not allowed. |
| sunglasses_not_allowed | Sunglasses are not allowed. |
| headwear_not_allowed | Hats or head coverings are not allowed. |
| face_mask_not_allowed | A face mask is not allowed. |
| face_covering_found | A face covering was detected. |
| expression_not_neutral | The expression is not neutral. |
| crop_bottom | Not enough space is visible below the shoulders. |
| crop_left | Not enough of the left shoulder is visible. |
| crop_right | Not enough of the right shoulder is visible. |
| head_turned | The head is turned too far to the side. |
| head_tilted | The head is tilted too far up or down. |
| not_in_color | The photo must be in color. |
| other | The photo did not meet a requirement. |
- • Previews and finals are served only from SVOYAGER-hosted, authenticated URLs (
preview_url/final_url). Access requires your API key and the correct scope — the underlying storage is never exposed publicly. - • Uploaded images are private and scoped to your account and environment; upload targets are short-lived and single-use.
- • Document photos are processed to produce your result and are not shared across accounts.
See SVOYAGER’s Privacy Policy and Partner Agreement & Terms for data-processing, retention and deletion details.