Developers

The public API

A read-only JSON catalogue of what the studio makes: the portrait styles, the products with their exact prices, and the countries we ship to. It is built with the site, so it says what the site says. The catalogue needs no key or account.

What we sell: AI-rendered, painted-style portraits of cats and dogs, printed on canvas or on a phone case. A print, not a painting. Every proof is free to review before anything is bought.

Quickstart

For an approved platform, a proof takes four steps.

  1. Get a connector key: write to the contact address below. Keep the key on your server.
  2. Start a proof: POST the pet photo, a style id from /api/v1/styles and the user's email address. The answer is 202 with the proof's id.
  3. Poll the proof: GET it every few seconds until its status is ready or failed.
  4. Show the result: imageUrl is the proof image, and checkoutUrl is our page where the user sees it on a canvas print or a phone case and pays.

Live endpoints

Each is a GET request that returns JSON and may be called from any origin. The responses are static files, refreshed when the site is rebuilt and cacheable for five minutes.

  • GET /api/v1 What this API is, its endpoints, its documentation and its contact.
  • GET /api/v1/styles The portrait styles on offer, with sample images and each style's page.
  • GET /api/v1/products Canvas sizes and phone-case styles with exact prices, and the shipping countries.
  • GET /api/v1/openapi.json This API described in OpenAPI 3.1.

OpenAPI 3.1 description

Example

curl https://pawforgeart.com/api/v1/products
const res = await fetch("https://pawforgeart.com/api/v1/styles");
const { styles } = await res.json();

A shortened response from the products endpoint:

{
  "currency": "USD",
  "canvas": {
    "sizes": [
      {
        "label": "12\" × 16\"",
        "widthIn": 12,
        "heightIn": 16,
        "orientation": "vertical",
        "priceCents": 6000
      }
    ]
  },
  "phoneCase": {
    "styles": [
      {
        "id": "impact",
        "label": "Impact-resistant case",
        "priceCents": 4500
      }
    ],
    "devices": null
  },
  "shipping": {
    "countryCount": 32,
    "countries": [
      {
        "code": "US",
        "name": "United States"
      },
      {
        "code": "CA",
        "name": "Canada"
      }
    ]
  }
}

We ship to 32 countries, listed in the products endpoint.

Authentication

The read endpoints are open: no key and no account. The proof endpoints need a connector key, sent in the X-PawForge-Key header; we issue keys to approved platforms.

Keep the key on your server: the routes send no CORS headers, so a browser cannot call them.

Proof API, for approved platforms

Two keyed endpoints on https://api.pawforgeart.com: start a free proof from a pet photo, then read it back. A ready proof carries a link to our checkout page, which shows the product preview; purchases complete through our Stripe checkout. There is no public sign-up for a key.

Live for approved platforms: callable only with a connector key the studio issues. There is no public sign-up; write to the contact address to be considered.

POST /api/v1/proofs

Start a free proof: send a pet photo, a style id, the user's email address, and an optional orientation (multipart/form-data). Answers 202 with the proof's id.

Headers

NameRequiredDescription
X-PawForge-KeyYesYour connector key.

Form fields (multipart/form-data)

NameTypeRequiredDescription
photofileYesThe pet photo: JPEG, PNG or WebP, judged by its bytes, at most 12 MiB. The whole request may be at most 12,648,448 bytes.
styleIdstringYesA style id from /api/v1/styles.
emailstringYesThe user's email address. The proof email goes to it, and the free allowance is counted per address.
orientationstringNoOne of vertical, horizontal or square. Default: vertical.

Responses

202 Accepted: the proof is being made.

The error codes this route can answer are in the Errors table below.

Example request

curl -X POST https://api.pawforgeart.com/api/v1/proofs \
  -H "X-PawForge-Key: $PAWFORGE_KEY" \
  -F photo=@pet.jpg \
  -F styleId=roman_emperor \
  -F email=someone@example.com \
  -F orientation=vertical

Example response

{
  "id": "<proof id>",
  "status": "processing"
}

GET /api/v1/proofs/{id}

Read a proof this key started: its status (processing, ready or failed) and, when ready, an image URL and a checkout URL.

Headers

NameRequiredDescription
X-PawForge-KeyYesYour connector key.

Path parameter

NameTypeDescription
idstringThe id from the 202 answer.

Responses

200 The proof, with its status.

The error codes this route can answer are in the Errors table below.

Example request

curl https://api.pawforgeart.com/api/v1/proofs/<proof id> \
  -H "X-PawForge-Key: $PAWFORGE_KEY"

Example response

{
  "id": "<proof id>",
  "status": "ready",
  "styleId": "roman_emperor",
  "imageUrl": "<proof image URL>",
  "checkoutUrl": "<our checkout page URL>",
  "reason": null
}

Placeholders in angle brackets stand for real values.

Proof statuses

GET reports one of three statuses.

StatusMeaning
processingThe proof is being made. Poll again in a few seconds. imageUrl and checkoutUrl are null.
readyDone. imageUrl is the proof image (about 1024 pixels, never the print file), and checkoutUrl is the page where the user chooses a product and pays.
failedThe proof could not be made. imageUrl and checkoutUrl are null, and reason says why when the cause is known: no_pet_in_photo means no animal was found in the photo, so send a photo of the pet instead.

Errors

Every error is JSON of the form {"error": "<code>"}. A 400 also names the form field at fault, when there is one.

StatusCodeSent byMeaning
400invalid_requestPOSTA form field is missing or not valid; `field` names it when one field is to blame.
401unauthorizedPOST, GETNo key, or a key we do not recognise. The answer carries a WWW-Authenticate header naming the key header.
403allowance_exhaustedPOSTThis email address has used its free proofs for now. No wait time is given.
404not_foundGETNo proof with this id was started by this key.
413photo_too_largePOSTThe photo or the whole request is over its size limit. Check sizes on your side too: a far larger body can be refused before it reaches us, without a JSON answer.
415unsupported_photo_typePOSTNot a JPEG, PNG or WebP by its bytes, or a file we cannot safely strip of its metadata (for example a truncated or animated one).
429connector_daily_limitPOSTThis key has used its proofs for the UTC day. Retry-After gives the seconds to the next UTC midnight.
500internal_errorPOST, GETSomething unexpected went wrong. Retrying the same request is safe: within the retry window it returns the same proof rather than starting a second.
503unavailablePOSTThe photo could not be stored, so nothing was started. Retrying is safe.

Example error

{
  "error": "invalid_request",
  "field": "styleId"
}

Limits

LimitValue
Catalogue endpointsNo quota
Free proofs per email address10 every 30 days, 20 once the address is confirmed
Proofs per connector key24 per UTC day, by default
Photo size12 MiB
Request size12,648,448 bytes
Retry window2 minutes: a repeated POST returns the same proof

Versioning

This is version 1.2.0, and the paths carry /api/v1. This page and the OpenAPI document read their fields, limits and error codes from the same code the API runs, so they change when it does.

Contact

Questions, or a platform that would like to be considered for a connector key? Write to curator@pawforgeart.com.