VR9 AdKit API
One REST API for every AdKit tool: JSON in, a finished asset out in the same request. Authenticate with an API key and pay per generation from the same token balance you use in the workspace.
Base URL: https://adkit.vr9.pro/api/v1
Getting started
- 1. Create an API key
- Open the workspace, go to API keys and create one. The key is shown once — store it on your server, never in a browser or a mobile app. Revoke it there at any time. Sign in with Google to create one →
- 2. Authenticate every request
- Send the key in the Authorization header as a Bearer token. The X-API-Key header works too. Public reference endpoints (lists of options) need no key.
HTTP GET https://adkit.vr9.pro/api/v1/public/cards/options Authorization: Bearer adk_live_…your key… # the same key in a custom header also works: X-API-Key: adk_live_…your key… - 3. Pay per generation
- Every generation spends tokens from the balance of the account that owns the key. Tokens are charged only when an asset actually comes back — a failed request costs nothing — and every response includes your remaining balance so you can top up before it runs out.
Services
Each AdKit tool exposes its own endpoints under the same base URL and the same key. Pick a service:
Card Generator turns a title, a tagline, a few attributes and your photos into a finished listing card: a PNG rendered from a real HTML/CSS template, so every price and label stays exactly as typed. Photos come by URL in a JSON body, as files in a multipart body, or not at all — then the background is generated by the image model. The style is derived from your seller's id, so a marketplace with a thousand sellers gets a thousand consistent, distinct looks without anyone picking a template.
About the tool, with example cards →
Endpoints
| Endpoint | Auth | Purpose |
|---|---|---|
| GET/public/cards/options | none | Everything the request accepts: card types, aspect ratios with their output sizes, every style key, icons, limits and prices. Read it instead of hard-coding lists — the registries grow. |
| GET/public/cards/palettes | none | Colour palettes with their hex values, for building a picker. |
| GET/public/cards/style-presets | none | Style presets (moods) and which layouts, fonts and decorations each one allows. |
| POST/public/cards | API key | Generate a card from a JSON body. Photos by URL, or none to have the background generated. |
| POST/public/cards/upload | API key | The same as multipart/form-data: the JSON goes in the payload field, photos in images (repeatable), a portrait or logo in avatar. |
| GET/public/cards/{id} | API key | Metadata of a card you generated: seller, type, size, the style it got, tokens charged. |
| GET/public/cards/{id}/image.png | API key | The PNG of a card you generated. Needs the key, like every card route — image_url in the response points here. |
| POST/public/cards/drafts/{draft_id}/render | API key | Retry the render of a draft after a 503. Nothing was charged for the draft, and the generated background is reused. |
Quick start
One request, one card back. Replace the key and the photo URL:
curl -X POST "https://adkit.vr9.pro/api/v1/public/cards" \
-H "Authorization: Bearer $ADKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"seller_id": "seller-1042",
"card_type": "product",
"title": "Leather weekender bag",
"tagline": "Full-grain leather, brass hardware, built for 20 years",
"aspect_ratio": "4:5",
"attributes": [
{"label": "Price", "value": "$249", "icon": "price-tag"},
{"label": "Ships in", "value": "2 days", "icon": "truck"},
{"label": "Handmade", "icon": "star"}
],
"image_urls": ["https://cdn.example.com/products/1042/front.jpg"]
}'curl -X POST "https://adkit.vr9.pro/api/v1/public/cards/upload" \
-H "Authorization: Bearer $ADKIT_API_KEY" \
-F 'payload={"seller_id":"seller-1042","card_type":"product","title":"Leather weekender bag","tagline":"Full-grain leather, brass hardware"}' \
-F "images=@front.jpg" \
-F "images=@side.jpg" \
-F "avatar=@logo.png"const res = await fetch("https://adkit.vr9.pro/api/v1/public/cards", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ADKIT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
seller_id: "seller-1042",
card_type: "service",
title: "Same-day bike repair",
tagline: "Brakes, gears and tyres — fixed while you wait",
attributes: [
{ label: "From", value: "$29", icon: "price-tag" },
{ label: "Duration", value: "45 min", icon: "clock" },
{ label: "Location", value: "Berlin, Mitte", icon: "location" },
],
background_prompt: "a bright bicycle workshop, tools on a pegboard",
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const card = await res.json();
// card.image_base64 -> "data:image/png;base64,..." ; card.from_scratch === trueRequest fields
| Field | Type | Description |
|---|---|---|
| seller_idrequired | string | Your own stable identifier of the seller or brand. It drives the automatic style: the same id always gets the same layout, fonts and decoration, and different ids land on visibly different cards. Never printed on the card. |
| card_type | "product" | "service" | "freelancer" | "generic"default "generic" | How the attributes are laid out: a spec grid for a product, duration and location for a service, a contact block for a freelancer, a plain list otherwise. |
| titlerequired | string | Headline of the card. Up to … characters. |
| taglinerequired | string | One line under the headline. Up to … characters. |
| aspect_ratio | "1:1" | "4:5" | "3:4" | "5:4" | "4:3" | "16:9" | "9:16"default "4:5" | Card format. The pixel size of every format is in output_sizes of the options endpoint. |
| attributes | { label, value?, icon? }[]default [] | Short facts on the card — price, delivery, rating, contacts. value may be empty for a standalone fact such as “Handmade”; icon is a key from the icons list. Up to …. |
| image_urls | string[]default [] | Public https URLs of product photos: PNG, JPEG or WebP, up to … each, up to … per card. The first photo drives the palette; several photos become a collage. Empty, with no files uploaded, means “generate the background”. |
| avatar_url | string | Portrait or logo. Shown only by layouts that have an avatar slot (has_avatar_slot in the skeletons list); other layouts ignore it. |
| palette | string | Pin the colour palette to a key from the palettes list. By default the palette is matched to the first photo. |
| palette_mode | "image" | "seller"default "image" | How the palette is chosen when palette is not set. image: from the first photo, so a seller's products may differ in colour. seller: from seller_id only, so every card of the seller shares one palette. |
| style | string | A mood from the style presets: narrows layout, fonts and decoration to a matching set. The choice inside the set is still made per seller. |
| style_seed | string | Re-rolls the automatic style for the same seller_id without changing it. The same seed always gives the same style — use it for “show me another look”. |
| skeleton | string | Pin the layout to a key from the skeletons list. Wins over style and seller_id. |
| font_pair | string | Pin the font pairing to a key from the font pairs list. |
| accent | string | Pin the background decoration to a key from the accents list. |
| framing | string | Pin how the photo is framed to a key from the framings list. |
| background_prompt | string | Only when the background is generated: what should be in the picture (“on a wooden table by a window”). Text and logos are always kept out of the background. Up to … characters. |
Photos: yours or generated
There are three ways to get a picture onto the card, and the price follows the choice:
- By URL
- Put public https links in image_urls. The server fetches them itself — no redirects, … max, PNG, JPEG or WebP by content, not by file name.
- As files
- Use the upload endpoint with the JSON in payload and the files in images — the way to go when photos are not publicly reachable.
- Generated
- Send no photos at all and the image model paints a background from the title, the tagline and background_prompt. The response has from_scratch: true and the higher price applies.
… token(s) per card with your photos, … with a generated background.
One style per seller, many products
The style is not random per request — it is derived from seller_id. Layout, fonts, decoration and photo framing are fixed for a seller, whatever product you send. The palette is the one axis that by default follows the photo, so a seller with a red bag and a blue jacket may get two palettes.
Pick how strict you want the seller's look to be:
- Same look, colours adapt to each photo — the default. Send seller_id and nothing else.
- Same look, same colours — add palette_mode: "seller", or pin palette to a key (for example the one returned in style.palette of the seller's first card).
- Another look for the same seller — add style_seed. Change the seed to re-roll, keep it to stay.
- Exactly this look — pin skeleton, font_pair, accent, framing and palette. Nothing is left to chance. style (a preset) narrows the choice without pinning it.
Precedence, most specific first: an explicit key (skeleton, palette, …) beats a style preset, which beats seller_id + style_seed. The style actually used comes back in every response under style, so you can store it and pin it later.
import base64
import requests
BASE = "https://adkit.vr9.pro/api/v1"
HEADERS = {"Authorization": f"Bearer {ADKIT_API_KEY}"}
def render_catalogue(seller: dict, products: list[dict]) -> None:
for product in products:
body = {
"seller_id": seller["id"], # one look for the whole seller
"palette_mode": "seller", # ...including the colours
"card_type": "product",
"title": product["name"],
"tagline": product["short_description"],
"attributes": [
{"label": "Price", "value": product["price"], "icon": "price-tag"},
],
"image_urls": product["photo_urls"], # [] -> background is generated
}
r = requests.post(f"{BASE}/public/cards", headers=HEADERS, json=body, timeout=120)
if r.status_code == 503: # renderer busy: saved as a draft, nothing charged
retry_path = r.json()["detail"]["retry_path"]
r = requests.post(f"{BASE}{retry_path.removeprefix('/api/v1')}", headers=HEADERS, timeout=120)
r.raise_for_status()
card = r.json()
png = base64.b64decode(card["image_base64"].split(",", 1)[1])
with open(f"cards/{product['id']}.png", "wb") as f:
f.write(png)
print(product["id"], card["style"], card["token_balance"])Response
The card is in the response itself — no second request needed. image_base64 is a data URI you can decode and store; image_url is the same PNG behind the key.
| Field | Type | Description |
|---|---|---|
| id | string | Card id — use it with the metadata and image routes. |
| image_url | string | Path of the PNG on this API. Needs the key; it is not a public link. |
| image_base64 | string | The PNG as a data:image/png;base64 URI. |
| width | number | Width of the PNG in pixels. |
| height | number | Height of the PNG in pixels. |
| style | { skeleton, palette, font_pair, accent, framing } | The style actually used. Pin any of these keys in a later request to reproduce it. |
| from_scratch | boolean | true when the background was generated by the model. |
| tokens_charged | number | Tokens spent on this card. |
| token_balance | number | Tokens left on the account after this card. |
{
"id": "0f1c9d2e-5b7a-4c3e-9f1d-2a6b8c4d7e90",
"image_url": "/api/v1/public/cards/0f1c9d2e-5b7a-4c3e-9f1d-2a6b8c4d7e90/image.png",
"image_base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
"width": 2560,
"height": 3200,
"style": {
"skeleton": "split-hero",
"palette": "linen-coral",
"font_pair": "modern-sans",
"accent": "clean",
"framing": "rounded",
"attribute_partial": "partials/attributes_spec_grid.html.jinja"
},
"from_scratch": false,
"tokens_charged": 1,
"token_balance": 41
}When the renderer is busy
Rendering runs in a shared headless browser. If it is unavailable for a while, the request returns 503 with a draft_id: the card — including any generated background — is saved, nothing is charged, and you retry with a single POST to retry_path. Retries never call the image model again.
HTTP/1.1 503 Service Unavailable
{
"detail": {
"code": "render_unavailable",
"draft_id": "5b2e7c1a-9d4f-4e8b-a3c6-1f0d2e9b7a54",
"attempts": 3,
"retry_path": "/api/v1/public/cards/drafts/5b2e7c1a-9d4f-4e8b-a3c6-1f0d2e9b7a54/render",
"message": "The renderer is busy. Your card is saved as a draft and no tokens were charged — retry it with POST /api/v1/public/cards/drafts/5b2e7c1a-.../render."
}
}Card-specific errors
| Status | Meaning |
|---|---|
| 413 | An uploaded or fetched image exceeds the size limit. |
| 415 | A file is not PNG, JPEG or WebP — the format is detected from the bytes, not the file name. |
| 422 | Besides validation: an image URL that could not be fetched, redirected, resolved to a private address, or returned something that is not an image. Nothing is charged. |
| 503 | Renderer unavailable; the card is saved as a draft — see above. |
Limits
Defaults below; the live values for this server are in the options endpoint.
- … generation requests per minute per key
- … photos per card, … each
- … attributes per card
- title up to … characters, tagline up to …
Errors
Errors come back as JSON with a detail field; validation errors (422) list the offending fields. The codes below are shared by every service — service-specific ones are listed in the service's own section.
| Status | Meaning |
|---|---|
| 401 | Missing, invalid or revoked API key. |
| 402 | Not enough tokens on the account. Nothing was generated or charged — top up and retry. |
| 422 | The request failed validation: a missing field, an unknown key, a value out of range. |
| 429 | Rate limit exceeded for this key. Wait a minute and retry; each service's options endpoint returns its limit. |
Ready to integrate?
Create a key and send the first request in minutes — or try the same generator by hand in the workspace first.