# Zaps Agentic API

Photos and a sentence in, finished designs out.

`designs` does the whole job in one call. `search` and `fill` are the same work
in two steps, for when you want to choose the template yourself. `uploadUrl`
gets local photos somewhere the renderer can reach.

Search and upload are free. Each rendered design spends one agentic token, and
refunds it if the render fails.

Base url: `https://api.zaps.design`

## Authentication

**Most agents should not use a key at all.** The MCP server carries every
operation with its schema and authorizes by browser sign-in:

```
claude mcp add --transport http zaps https://api.zaps.design/mcp
```

Searching, uploading and the first design work with no account. When a call
needs one, the server answers `401` with a `WWW-Authenticate` header pointing
at `https://api.zaps.design/.well-known/oauth-protected-resource`; follow it and
the client opens a browser for the person to approve. Nothing is pasted anywhere.

For the REST API, every request carries a key:

```
Authorization: Bearer zak_...
```

Keys start with `zak_` and are minted by the account holder from a signed-in
session (`POST /api/v1/agentic/keys`); there is no self-service issuance. A key
is shown once, at creation — it is stored as a hash, so a lost key is replaced
rather than recovered. Revoking takes effect immediately.

Malformed, unknown and revoked keys all answer `401 invalid API key`. That is
deliberate: distinguishing them would let anyone test guesses against the
endpoint and learn which ones were real.

## POST /api/v1/agentic/designs

The whole job: find templates that fit the brief, render each with your photos,
return finished designs.

```bash
curl -s -X POST https://api.zaps.design/api/v1/agentic/designs \
  -H "Authorization: Bearer $ZAPS_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"brief":"thanksgiving family dinner invite","images":["https://.../a.jpg"],"count":5}'
```

| field | type | notes |
|---|---|---|
| `brief` | string, required | the search query, written as Title Case phrases separated by commas |
| `images` | array of urls | placed in order into each template's image slots |
| `count` | integer, 1–10 | defaults to 5 |
| `segment` | string | `story`, `carousel` or `invite` |

Each option carries `imageUrl` (the finished design), `editorUrl` (where a
person opens it to keep editing), `label` and `coverId`. An option that could
not be rendered carries `failed` instead and the others still stand — a set
where some rendered is a success, not an error.

The response also carries `entitlement`, and when fewer designs came back than
were asked for, `upsell` and `requested`. One design is free for everyone; the
full set is what a Pro or Premium plan buys.

## GET /api/v1/uploadUrl

Our renderer fetches images over the network, so a path on your machine can
never be used. This presigns somewhere to put them.

```bash
curl -s "https://api.zaps.design/api/v1/uploadUrl?reason=generation&mimeType=image/jpeg&imageCount=2" \
  -H "Authorization: Bearer $SESSION" -H "x-profile: $PROFILE"
```

Returns `uploadUrl` plus `imageCount` more in `uploadUrls`. `PUT` each file to
its url with the same `Content-Type`; the object then reads back at that url
with the query string removed, which is what you pass as an image.

Over MCP this is the `upload_images` tool, which needs no session and hands back
both halves of each pair already worked out.

## POST /api/v1/agentic/search

```bash
curl -s -X POST https://api.zaps.design/api/v1/agentic/search \
  -H "Authorization: Bearer $ZAPS_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"query":"thanksgiving family dinner invite","limit":5}'
```

| field | type | notes |
|---|---|---|
| `query` | string, required | Title Case phrases separated by commas, the way the catalogue describes itself |
| `limit` | integer, 1–25 | defaults to 5 |
| `segment` | string | `story`, `carousel` or `invite` |

Each hit carries `coverId` (what `fill` takes), `description` (what the
template actually is — read this to choose), `thumbnailUrl`, `segment`,
`type`, `score` and `editorUrl`.

The index is built from each template's own description, which is Title Case phrases
separated by commas. A query in that register matches markedly better than the same
intent as a sentence — about 0.11 higher, measured against the live catalogue:
`"Birthday Party, Friends, Celebration, Confetti, Party Invite"` scores 0.868 where
`"a birthday party invitation for my friend"` scores 0.779. A single generic word is
worst of all: `"template"` matches everything equally and tells you nothing.

Scores are cosine similarity. Above ~0.70 is a confident match; a top score
around 0.65 usually means the catalogue has nothing for that occasion and you
are being handed the nearest neighbour. Say so rather than rendering it.

`matchedBy` is `semantic` or `text`. `text` means the ranking model was
unavailable and the results are weaker.

## POST /api/v1/agentic/fill

```bash
curl -sD /dev/stderr -X POST https://api.zaps.design/api/v1/agentic/fill \
  -H "Authorization: Bearer $ZAPS_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"coverId":"<coverId from search>","images":["https://.../a.jpg"]}'
```

| field | type | notes |
|---|---|---|
| `coverId` | string | `coverId` from a search hit |
| `scene` | string | a scene url, if you already hold one — an alternative to `coverId` |
| `images` | array of urls | placed in order into the template's image slots |

The accounting comes back in headers, which is the only place it appears:

```
X-Agentic-Tokens-Cost: 1
X-Agentic-Tokens-Remaining: 499
```

## Status codes

| code | meaning |
|---|---|
| `200` | done |
| `400` | the request is malformed, or the template cannot be rendered — it is a sticker or a font, not a template |
| `401` | key missing, unknown or revoked |
| `402` | authenticated and valid, but the agentic token balance is empty |
| `5xx` | our side; the token is refunded automatically |

`402` rather than `403` is intentional: the caller is authenticated and the
request is fine, it just needs paying for. That is what lets an agent tell
"top up" apart from "fix your request".

## Using it from an agent

- **MCP**, no install and no key: `claude mcp add --transport http zaps https://api.zaps.design/mcp`
- **Skill**: https://zaps.design/.well-known/agent-skills/index.json
- **OpenAPI**: https://zaps.design/openapi.json
- **Machine-readable index of all of the above**: https://zaps.design/.well-known/api-catalog
