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.
- Get a connector key: write to the contact address below. Keep the key on your server.
- 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.
- Poll the proof: GET it every few seconds until its status is ready or failed.
- 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
| Name | Required | Description |
X-PawForge-Key | Yes | Your connector key. |
Form fields (multipart/form-data)
| Name | Type | Required | Description |
photo | file | Yes | The 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. |
styleId | string | Yes | A style id from /api/v1/styles. |
email | string | Yes | The user's email address. The proof email goes to it, and the free allowance is counted per address. |
orientation | string | No | One 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
| Name | Required | Description |
X-PawForge-Key | Yes | Your connector key. |
Path parameter
| Name | Type | Description |
id | string | The 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.
| Status | Meaning |
processing | The proof is being made. Poll again in a few seconds. imageUrl and checkoutUrl are null. |
ready | Done. 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. |
failed | The 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.
| Status | Code | Sent by | Meaning |
| 400 | invalid_request | POST | A form field is missing or not valid; `field` names it when one field is to blame. |
| 401 | unauthorized | POST, GET | No key, or a key we do not recognise. The answer carries a WWW-Authenticate header naming the key header. |
| 403 | allowance_exhausted | POST | This email address has used its free proofs for now. No wait time is given. |
| 404 | not_found | GET | No proof with this id was started by this key. |
| 413 | photo_too_large | POST | The 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. |
| 415 | unsupported_photo_type | POST | Not 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). |
| 429 | connector_daily_limit | POST | This key has used its proofs for the UTC day. Retry-After gives the seconds to the next UTC midnight. |
| 500 | internal_error | POST, GET | Something unexpected went wrong. Retrying the same request is safe: within the retry window it returns the same proof rather than starting a second. |
| 503 | unavailable | POST | The photo could not be stored, so nothing was started. Retrying is safe. |
Example error
{
"error": "invalid_request",
"field": "styleId"
}
Limits
| Limit | Value |
| Catalogue endpoints | No quota |
| Free proofs per email address | 10 every 30 days, 20 once the address is confirmed |
| Proofs per connector key | 24 per UTC day, by default |
| Photo size | 12 MiB |
| Request size | 12,648,448 bytes |
| Retry window | 2 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.