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
| 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/designs | none | The design catalog: every key the design field accepts, with the layout and style axes it is made of. |
| GET/public/cards/palettes | none | Colour palettes with their hex values, for building a picker. |
| GET/public/cards/style-presets | none | Style presets (moods): how many designs each one holds and which layouts and fonts they use. |
| 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://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 -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]"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 === trueRequest fields
| Field | Type | Description |
|---|---|---|
| seller_idrequired | string | Your 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. |
| 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 …. 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_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. |
| design | string | Pin 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. |
| style | string | A 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_seed | string | Re-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”. |
| skeleton | string | Pin the layout to a key from the skeletons list. The rest of the look is taken from a design on that layout. |
| font_pair | string | Override the font pairing with a key from the font pairs list. |
| title_style | string | Override the title treatment (caps, marker, underline, …) with a key from the title styles list. |
| attrs_style | string | Override the attribute list form (chips, pills, table, checklist, …) with a key from the attribute styles list. |
| hero_style | string | Override 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. |
| accent | string | Override the background decoration with a key from the accents list. |
| framing | string | Override how the photo is framed with a key from the framings list. Layouts with a full-bleed or shaped photo ignore it. |
| surface | string | Override 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). |
| mirror | boolean | true or false: use the mirrored variant of the layout (photo on the other side). Layouts without one ignore it. |
| 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. |
| ai_design | booleandefault false | Unique 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_brief | string | With 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.
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.
| 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 | { 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_scratch | boolean | true when the background was generated by the model. |
| ai_design | "new" | "reused" | "fallback" | null | Only 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_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": {
"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.
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.