MerryAd API

One REST API for every MerryAd 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://merryad.com/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://merryad.com/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 MerryAd 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, with text sized to fit whatever its length. 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 design is derived from your seller's id out of a catalog of well over a hundred designs, so a marketplace full of sellers gets consistent, distinct looks without anyone picking a template.

About the tool, with example cards →
Design gallery — every design with its key →

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/designsnoneThe design catalog: every key the design field accepts, with the layout and style axes it is made of.
GET/public/cards/palettesnoneColour palettes with their hex values, for building a picker.
GET/public/cards/style-presetsnoneStyle presets (moods): how many designs each one holds and which layouts and fonts they use.
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://merryad.com/api/v1/public/cards" \
  -H "Authorization: Bearer $MERRYAD_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://merryad.com/api/v1/public/cards/upload" \
  -H "Authorization: Bearer $MERRYAD_API_KEY" \
  -F 'payload={"seller_id":"seller-1042","card_type":"product","title":"Leather weekender bag","tagline":"Full-grain leather, brass hardware"}' \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "[email protected]"
Node.js — no photos, generated background
const res = await fetch("https://merryad.com/api/v1/public/cards", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MERRYAD_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 choice of design: the same id always gets the same design, and different ids land on visibly different cards. Never printed on the card.
card_type"product" | "service" | "freelancer" | "generic"default "generic"What the card is about. Steers the automatic choice towards designs recommended for that type (best_for in the designs list); every design can still be used for any type when you pass its key.
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 …. Designs with a hero element lift the first attribute out of the list and show it large — put the price or the discount first.
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.
designstringPin the whole look to a key from the designs list: layout, fonts, title treatment, attribute style, hero element, decoration, photo framing and surface. The palette is still chosen separately.
stylestringA mood from the style presets: narrows the catalog to the designs of that mood. The choice inside the mood is still made per seller. Ignored when design is set.
style_seedstringRe-rolls the automatic design for the same seller_id without changing it. The same seed always gives the same design — use it for “show me another look”.
skeletonstringPin the layout to a key from the skeletons list. The rest of the look is taken from a design on that layout.
font_pairstringOverride the font pairing with a key from the font pairs list.
title_stylestringOverride the title treatment (caps, marker, underline, …) with a key from the title styles list.
attrs_stylestringOverride the attribute list form (chips, pills, table, checklist, …) with a key from the attribute styles list.
hero_stylestringOverride the hero element — the first attribute shown large as a sticker or an inline figure — with a key from the hero styles list; none keeps it in the list.
accentstringOverride the background decoration with a key from the accents list.
framingstringOverride how the photo is framed with a key from the framings list. Layouts with a full-bleed or shaped photo ignore it.
surfacestringOverride which palette colour fills the text field: base, tint, accent, gradient or inverse (the palette flipped — a light palette gives a dark card and vice versa).
mirrorbooleantrue or false: use the mirrored variant of the layout (photo on the other side). Layouts without one ignore it.
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.
ai_designbooleandefault falseUnique design: the card is designed from scratch by an AI model instead of being picked from the catalog. design, palette and the single axes are ignored; style is passed on as a mood hint. The design is created once per seller_id + style_seed + brief + aspect_ratio + card_type and reused after that. Costs extra tokens once, when the design is created (see pricing).
ai_design_briefstringWith ai_design: what the design should look like (“Fiverr gig thumbnail, bold and bright”). Without it the seller ID picks a style direction. 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.

Unique AI design (ai_design: true): +… tokens once, when the design is created. Cards that reuse it cost the usual price.

Own Replicate key: save your Replicate API token once on the Tokens page of your account, and every API key you create runs the AI models on your Replicate account. Requests stay the same — nothing extra to send. In return, a card with a generated background costs … token(s) and a unique AI design +…. Cards from your own photos cost the same either way: they never call Replicate.

One style per seller, many products

The look is not random per request — it is derived from seller_id: one design from the catalog, 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 design and palette. Nothing is left to chance. Single axes (skeleton, font_pair, title_style, …) can be overridden on top; style (a preset) narrows the automatic choice without pinning it.

Precedence, most specific first: an explicit axis key (skeleton, font_pair, palette, …) beats design, which beats a style preset, which beats seller_id + style_seed. The look 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://merryad.com/api/v1"
HEADERS = {"Authorization": f"Bearer {MERRYAD_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"])

Text of any length

A layout fixes where text goes; the type size is then fitted to that space in the browser before the screenshot. Short text grows to fill it, long text shrinks, and the longest word of the title always stays on one line.

  • Everything is shown as long as it stays comfortably readable.
  • If it would get too small, attributes are dropped from the end of the list first — so order them by importance. They remain in your request and in the metadata; they are just not drawn.
  • Only when the text still does not fit at the minimum size are lines truncated with an ellipsis: the tagline first, then the title. A value is never cut while a label can still give way.
  • Within the documented limits a normal title, tagline and four to six attributes fit every design in every aspect ratio without truncation.

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{ design, skeleton, palette, font_pair, title_style, attrs_style, hero_style, accent, framing, surface, mirror }The look actually used: the design key (null when you overrode an axis and the result is no longer a catalog design) and every axis. Pin design — or the single axes — in a later request to reproduce it.
from_scratchbooleantrue when the background was generated by the model.
ai_design"new" | "reused" | "fallback" | nullOnly with ai_design: new — the design was created by this request (the design fee is included in tokens_charged); reused — an existing design was used; fallback — no design could be made, the card uses a catalog design and the fee was not charged.
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": {
    "design": "split-hero-03",
    "skeleton": "split-hero",
    "palette": "linen-coral",
    "font_pair": "modern-sans",
    "accent": "dot-grid",
    "framing": "rounded",
    "title_style": "lower",
    "attrs_style": "flags",
    "hero_style": "big",
    "surface": "inverse",
    "mirror": true
  },
  "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.