Developer Docs

External API for permissioned image access

Model Me's external API lets a business create approval requests, wait for customer consent, and pull signed image URLs only after permission is active. This page documents the public business API that is implemented in the current prototype.

Base path

/api/v1/external

Use your deployed API host plus the external API prefix.

Auth

Bearer ak_...

Every request is authorized with a Clerk organization API key.

Core flow

request -> approve -> fetch images

Create a request, wait for approval, then read signed image URLs.

Overview

The external API is designed for business integrations that need a durable approval record before using a customer's likeness.

Recommended flow

  1. 1Create a permission request and store the returned request_id and approval_url.
  2. 2Send the approval_url to the customer or open it inside your own workflow.
  3. 3Poll the request status until it becomes approved, denied, expired, revoked, or canceled.
  4. 4When approved, fetch signed image URLs from the images endpoint and download them before they expire.

Implementation notes

  • The production FastAPI Swagger docs are disabled, so this page is the public reference for now.
  • All endpoint responses are wrapped in a top-level data object on success.
  • Errors always use the shape { error: { code, message } }.
  • Signed image URLs are temporary and should be downloaded or copied into your own pipeline promptly.

Authentication

This API uses business-scoped API keys, not browser sessions and not consumer auth tokens.

Send Authorization: Bearer <api_key> on every request.

The API key must be a Clerk organization API key and currently starts with ak_.

Keys are verified against Clerk, then mapped to the local business tied to that organization.

User JWTs are not accepted on this surface. This API is for server-to-server business integrations.

Authorization: Bearer ak_live_your_org_key

Create request body

POST /requests accepts the fields below. Validation is enforced server-side.

Field
Required
Description
reference_id
No
Business-side idempotency key. Reusing the same value for the same business returns the existing request instead of creating a duplicate.
customer_name
No
Optional label shown in the request record. Max 255 characters.
customer_email
No
Optional email stored with the request. Max 255 characters.
redirect_url
No
Optional URL passed through the approval flow. HTTP and HTTPS are accepted so local testing flows can redirect back cleanly.
webhook_url
No
Accepted and stored with the request. In the current prototype, webhook delivery is not implemented yet.
request_reason
No
Optional freeform reason shown with the request. Max 1000 characters.
expires_in_hours
No
Request lifetime in hours. Defaults to 72. Allowed range: 1 to 720.

Status model

These are the request states currently returned by the external API.

pending

The request exists and has not been opened by the customer yet.

opened

The customer has opened the approval page but has not responded yet.

approved

The customer granted access. Images can be fetched while permission remains active.

denied

The customer explicitly declined the request.

expired

The request passed its expires_at timestamp and can no longer be used.

revoked

Access was previously granted and then revoked.

canceled

The business canceled the request before the customer responded.

Endpoint reference

Every endpoint below is implemented today under the external router in the FastAPI backend.

POST/requests

Create a permission request

Creates a new approval request and returns a hosted approval URL for the customer.

Rate limit: 60/minute

Notes

  • Returns 201 for a new request.
  • Returns 200 with the existing request when the same reference_id is reused for the same business.

Success envelope

Successful responses are returned as { data: ... }.

Example request

curl -X POST "$API_BASE_URL/api/v1/external/requests" \
  -H "Authorization: Bearer ak_live_your_org_key" \
  -H "Content-Type: application/json" \
  -d '{
    "reference_id": "order_1042",
    "customer_name": "Avery Stone",
    "customer_email": "avery@example.com",
    "redirect_url": "https://studio.example.com/model-me/return",
    "request_reason": "Approve image generation for your spring campaign",
    "expires_in_hours": 168
  }'

Example response

{
  "data": {
    "request_id": "0e54d952-8d9f-4cb5-bf6e-8a90973d8af7",
    "status": "pending",
    "reference_id": "order_1042",
    "created_at": "2026-04-02T18:14:11.205+00:00",
    "expires_at": "2026-04-09T18:14:11.205+00:00",
    "approval_url": "https://app.example.com/approve/6d2d..."
  }
}
GET/requests/{request_id}

Get request status

Polls a single request and returns its latest state. consumer_id is only included after approval.

Rate limit: 120/minute

Notes

  • Requests are lazily marked expired if expires_at is in the past.
  • opened_at and responded_at appear once those events happen.

Success envelope

Successful responses are returned as { data: ... }.

Example request

curl "$API_BASE_URL/api/v1/external/requests/0e54d952-8d9f-4cb5-bf6e-8a90973d8af7" \
  -H "Authorization: Bearer ak_live_your_org_key"

Example response

{
  "data": {
    "request_id": "0e54d952-8d9f-4cb5-bf6e-8a90973d8af7",
    "status": "approved",
    "reference_id": "order_1042",
    "created_at": "2026-04-02T18:14:11.205+00:00",
    "opened_at": "2026-04-02T18:16:08.481+00:00",
    "responded_at": "2026-04-02T18:17:02.110+00:00",
    "expires_at": "2026-04-09T18:14:11.205+00:00",
    "consumer_id": "090d17c2-9cf5-4d99-b1b3-6d18f44f93bc"
  }
}
GET/requests/{request_id}/images

Fetch approved customer images

Returns signed URLs and profile metadata after the request is approved and permission is still active.

Rate limit: 30/minute

Notes

  • Returns 403 until the request is approved.
  • Signed URLs currently expire after 1 hour.
  • Permissions revoked after approval also block this endpoint.

Success envelope

Successful responses are returned as { data: ... }.

Example request

curl "$API_BASE_URL/api/v1/external/requests/0e54d952-8d9f-4cb5-bf6e-8a90973d8af7/images" \
  -H "Authorization: Bearer ak_live_your_org_key"

Example response

{
  "data": {
    "consumer_id": "090d17c2-9cf5-4d99-b1b3-6d18f44f93bc",
    "profile": {
      "display_name": "Avery",
      "gender": "woman",
      "age_range": "25_34",
      "hair_color": "brown",
      "ethnicity": "multiracial",
      "body_type": "athletic"
    },
    "quality": {
      "score": 0.93,
      "flags": {
        "lighting": "good",
        "variety": "strong"
      }
    },
    "photos": [
      {
        "id": "44ab8a5a-cd3b-4f1b-a33a-ef5b5fe950fd",
        "signed_url": "https://supabase.example.com/storage/v1/object/sign/...",
        "file_name": "portrait-front.jpg",
        "mime_type": "image/jpeg",
        "angle_category": "front",
        "url_expires_at": "2026-04-02T19:17:33.411+00:00"
      }
    ],
    "photo_count": 1
  }
}
GET/requests

List requests for the current business

Returns paginated request history for the API key's business, optionally filtered by status.

Rate limit: 60/minute

Notes

  • Query params: status, limit, offset.
  • limit defaults to 50 and is capped at 100.
  • consumer_id is only exposed for approved requests.

Success envelope

Successful responses are returned as { data: ... }.

Example request

curl "$API_BASE_URL/api/v1/external/requests?status=approved&limit=20&offset=0" \
  -H "Authorization: Bearer ak_live_your_org_key"

Example response

{
  "data": {
    "requests": [
      {
        "request_id": "0e54d952-8d9f-4cb5-bf6e-8a90973d8af7",
        "status": "approved",
        "reference_id": "order_1042",
        "customer_name": "Avery Stone",
        "customer_email": "avery@example.com",
        "source": "api",
        "created_at": "2026-04-02T18:14:11.205+00:00",
        "responded_at": "2026-04-02T18:17:02.110+00:00",
        "expires_at": "2026-04-09T18:14:11.205+00:00",
        "consumer_id": "090d17c2-9cf5-4d99-b1b3-6d18f44f93bc"
      }
    ],
    "total": 1,
    "limit": 20,
    "offset": 0
  }
}

Errors and rate limits

Application errors are structured, machine-readable, and safe to surface in logs.

401 Unauthorized

{
  "error": {
    "code": "AUTH_TOKEN_INVALID",
    "message": "Expected a Clerk API key (ak_...)."
  }
}

403 Forbidden

{
  "error": {
    "code": "CONSUMER_NOT_CONNECTED",
    "message": "Request has not been approved."
  }
}

429 Too Many Requests

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Please retry after 30 seconds."
  }
}

If a request is rate-limited, the API also returns a Retry-After response header. Endpoint-specific limits are currently enforced per API key for the external router.

Next step

Need a business workspace first?

The external API is scoped to a business organization. If your team has not set up its Model Me business account yet, start there before wiring the integration.