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

EndpointAuthPurpose
GET/public/cards/optionsnoneEverything 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/palettesnoneColour palettes with their hex values, for building a picker.
GET/public/cards/style-presetsnoneStyle presets (moods) and which layouts, fonts and decorations each one allows.
POST/public/cardsAPI keyGenerate a card from a JSON body. Photos by URL, or none to have the background generated.
POST/public/cards/uploadAPI keyThe 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 keyMetadata of a card you generated: seller, type, size, the style it got, tokens charged.
GET/public/cards/{id}/image.pngAPI keyThe 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}/renderAPI keyRetry 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 — JSON, photos by 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 — multipart, photos as files
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"
Node.js — no photos, generated background
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 === true

Request fields

FieldTypeDescription
seller_idrequiredstringYour 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.
titlerequiredstringHeadline of the card. Up to … characters.
taglinerequiredstringOne 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_urlsstring[]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_urlstringPortrait or logo. Shown only by layouts that have an avatar slot (has_avatar_slot in the skeletons list); other layouts ignore it.
palettestringPin 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.
stylestringA 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_seedstringRe-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”.
skeletonstringPin the layout to a key from the skeletons list. Wins over style and seller_id.
font_pairstringPin the font pairing to a key from the font pairs list.
accentstringPin the background decoration to a key from the accents list.
framingstringPin how the photo is framed to a key from the framings list.
background_promptstringOnly 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.

Python — render a seller's catalogue in one style
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.

FieldTypeDescription
idstringCard id — use it with the metadata and image routes.
image_urlstringPath of the PNG on this API. Needs the key; it is not a public link.
image_base64stringThe PNG as a data:image/png;base64 URI.
widthnumberWidth of the PNG in pixels.
heightnumberHeight 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_scratchbooleantrue when the background was generated by the model.
tokens_chargednumberTokens spent on this card.
token_balancenumberTokens left on the account after this card.
JSON — response
{
  "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.

JSON — 503 with a draft
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

StatusMeaning
413An uploaded or fetched image exceeds the size limit.
415A file is not PNG, JPEG or WebP — the format is detected from the bytes, not the file name.
422Besides 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.
503Renderer 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.

StatusMeaning
401Missing, invalid or revoked API key.
402Not enough tokens on the account. Nothing was generated or charged — top up and retry.
422The request failed validation: a missing field, an unknown key, a value out of range.
429Rate 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.