# Meyer Launch customer API

Human guide: https://meyerlaunch.com/help/api
OpenAPI: https://meyerlaunch.com/help/api/openapi.json

## Start safely

Create a workspace key in Settings → API keys. Only owners issue/revoke keys; the UI issues 90-day keys shown once. Select only needed permissions. Store MEYER_LAUNCH_API_KEY on your backend, never in client code, URLs, chat or git. Send Authorization: Bearer <key> over HTTPS. Keys act within their workspace and require the issuing owner to retain owner access. Revoked/expired keys stop working. Marketing accounts connect separately in Settings.

## Read-only quick start

```sh
curl --fail-with-body --silent --show-error \
  "https://meyerlaunch.com/api/v1/projects" \
  --header "Authorization: Bearer $MEYER_LAUNCH_API_KEY"
```

```js
const key = process.env.MEYER_LAUNCH_API_KEY;
if (!key) throw new Error("Configure MEYER_LAUNCH_API_KEY on your server.");

const response = await fetch("https://meyerlaunch.com/api/v1/projects", {
  headers: { Authorization: `Bearer ${key}` },
});
const body = await response.json();
if (!response.ok) throw new Error(`API request failed (${response.status}): ${body.error}`);
console.log(body.data.map(({ id, name, slug }) => ({ id, name, slug })));
```

## Limits, credits and retries

60 accepted requests per key per minute; authenticated failed operations can consume a slot. Honor 429 and Retry-After (normally 60 seconds). Project/campaign reads return up to 100 records; task lists up to 200. No pagination cursor currently exists. Reads and event ingestion do not spend generation credits. Task preparation costs 1 workspace credit, refunded on failure. Agent calls use variable workspace credits and the agent monthly cap, without a quote step. Media generation requires a quote and separately approved maxCredits confirmation, with 50 confirmed credits per key per UTC day. Wallet balance and provider limits also apply. Reuse the same UUID requestKey AND identical payload for an uncertain retry; do not blindly create another paid request. A plan record is a draft,202 means queued, and neither proves publication or campaign delivery.

## GET /api/v1/projects

Read projects

Permission: projects:read
Credits: No generation credits

List projects in the key’s workspace. Use the project ID for plans, tasks and events; use its slug for agent and generation requests.

- Returns up to 100 projects, newest first. There is no pagination cursor.
- Fields: id, name, slug, kind and created_at. Project kinds are app, saas and business.

Example 200 response (selected fields):

```json
{
  "data": [
    {
      "id": "10000000-0000-4000-8000-000000000001",
      "name": "Example product",
      "slug": "your-project",
      "kind": "saas",
      "created_at": "2026-11-04T12:00:00Z"
    }
  ],
  "limit": 100
}
```


## GET /api/v1/campaigns

Read paid campaigns

Permission: campaigns:read
Credits: No generation credits

Read saved paid-campaign configuration, including status, platform, objective, currency, daily budget in cents, start date and end date.

- Returns up to 100 campaigns, newest first. There is no pagination cursor.
- This endpoint does not return a complete performance report or change a campaign’s budget or delivery. An empty data array means no records returned, not verified zero ad activity.

Example 200 response (selected fields):

```json
{
  "data": [],
  "limit": 100
}
```


## POST /api/v1/launch-plans

Create a campaign-plan draft

Permission: plans:write
Credits: No AI generation in this call

Create a draft plan for an existing project from a name, goal and optional notes. This creates the plan record; it does not generate every asset or launch a campaign.

- name: 1–120 characters. notes: up to 4,000 characters; defaults to an empty string.
- goal: traffic, leads, app_growth or awareness. leads requires a business project. For SaaS, start with traffic or awareness; app_growth is intended for mobile apps.
- Use a new UUID requestKey for a new plan. On a retry, keep the same UUID and identical input.

Example request:

```json
{
  "projectId": "10000000-0000-4000-8000-000000000001",
  "name": "Product introduction",
  "goal": "traffic",
  "notes": "Prepare drafts from the confirmed project brief.",
  "requestKey": "20000000-0000-4000-8000-000000000002"
}
```


Example 201 response (selected fields):

```json
{
  "id": "40000000-0000-4000-8000-000000000004",
  "state": "draft"
}
```


## GET /api/v1/tasks

List plan tasks

Permission: tasks:read
Credits: No generation credits

List tasks and check whether each can be prepared by Meyer Launch. Optional query filters select a project and task status.

- Returns up to 200 tasks ordered by due date, then priority. No pagination cursor is provided.
- status filter: active, completed, skipped or stale. preparable indicates whether the preparation endpoint supports this task.
- The list includes preparation status/error. Use task detail for prepared output.

Query filters:
- projectId: optional project UUID
- status: optional task status

Example 200 response (selected fields):

```json
{
  "data": [],
  "limit": 200
}
```


## GET /api/v1/tasks/{id}

Read a task and prepared output

Permission: tasks:read
Credits: No generation credits

Read one workspace task, its preparation state and any saved output. Response examples below show selected fields.

- Use the id returned by the task list. preparation contains status, error and output.
- The output varies by preparation kind: a review summary, brief gaps or draft copy. Check status and error before treating output as ready.

Example 200 response (selected fields):

```json
{
  "data": {
    "id": "30000000-0000-4000-8000-000000000003",
    "projectId": "10000000-0000-4000-8000-000000000001",
    "title": "Prepare a social post",
    "preparable": true,
    "prepareKind": "copy",
    "preparation": {
      "status": "none",
      "error": null,
      "output": null
    }
  }
}
```


## POST /api/v1/tasks/{id}/prepare

Prepare a supported task

Permission: tasks:prepare
Credits: 1 workspace credit per preparation; refunded on failure

Queue draft preparation for a task whose preparable field is true. The result still needs review in Meyer Launch.

- Supply a UUID requestKey. Retrying the same requestKey returns the same attempt without charging again.
- 202 means queued, not completed. Poll the task detail for preparation output.
- Unsupported tasks return 422. Preparation does not approve, publish or schedule the work.

Example request:

```json
{
  "requestKey": "20000000-0000-4000-8000-000000000002"
}
```


Example 202 response (selected fields):

```json
{
  "id": "30000000-0000-4000-8000-000000000003",
  "state": "queued",
  "requestKey": "20000000-0000-4000-8000-000000000002"
}
```


## POST /api/agent/chat

Message the marketing agent

Permission: agent:chat
Credits: Variable workspace credits; workspace balance and agent monthly cap apply

Ask the agent to read project context and propose marketing work. It acts as the workspace owner who issued the key. Responses stream as newline-delimited JSON, not one JSON document.

- For a new project conversation, send projectSlug and text. To continue, send conversationId and text.
- text is limited to 20,000 characters. The key remains restricted to its workspace and the issuing owner’s accessible conversations.
- Stream events include conversation, text (delta), status, proposal, notice, error and done. Preserve conversation.id for the next request. A stream can report an error after HTTP 200; wait for done and inspect error events.
- Costs depend on the run; this endpoint has no separate generation quote step. Configure the workspace’s agent credit cap before automating it. Draft proposals wait for approval in Meyer Launch.

Example request:

```json
{
  "projectSlug": "your-project",
  "text": "Read the brief and propose three social-post directions. Keep everything as drafts."
}
```


Example 200 response (selected fields):

```json
{
  "type": "conversation",
  "id": "40000000-0000-4000-8000-000000000004",
  "title": "Marketing directions",
  "projectSlug": "your-project"
}
```


## POST /api/v1/generations

Request a media quote

Permission: generations:write
Credits: Quote only; no generation credits spent until confirmation

Quote an image, narration or composed-video draft for a project. The image example is below; narration/video inputs and callbacks are described after the endpoint reference.

- Use projectSlug, UUID requestKey, kind (image, narration or video) and input. webhookUrl is optional.
- Same key + same requestKey + identical input returns the existing job. preparing means retry the same request; a failed quote requires a new requestKey.
- The sample 2-credit value is illustrative. Always use the live credits and expiresAt returned for your quote.
- Jobs belong to their issuing API key. A different key in the same workspace cannot read or confirm that job. Internal free Lab generation is outside this customer API.

Example request:

```json
{
  "projectSlug": "your-project",
  "requestKey": "20000000-0000-4000-8000-000000000002",
  "kind": "image",
  "input": {
    "prompt": "A blue notebook on a wooden desk",
    "size": "1K",
    "aspect": "1:1"
  }
}
```


Example 201 response (selected fields):

```json
{
  "id": "40000000-0000-4000-8000-000000000004",
  "kind": "image",
  "status": "quoted",
  "credits": 2,
  "expiresAt": "2026-11-04T12:15:00Z",
  "confirmedAt": null,
  "sourceId": "30000000-0000-4000-8000-000000000003",
  "creativeId": null,
  "resultUrl": null,
  "resultUrlExpiresIn": null,
  "error": null,
  "dailyCreditCap": 50
}
```


## POST /api/v1/generations/{id}/confirm

Confirm a reviewed quote

Permission: generations:write
Credits: Spends the quoted workspace credits

Approve the selected job’s credit charge with an explicit maxCredits ceiling. Ask the customer to approve the live quoted cost before calling this endpoint.

- maxCredits must be an integer from 0 to 50 and at least the quoted cost. Use the exact approved cost rather than an unnecessarily high ceiling.
- Each key can confirm up to 50 credits per UTC day, in addition to workspace balance and provider limits. Failed attempts keep their daily-cap reservation; normal generation-failure wallet refunds still apply.
- Repeating confirmation uses the same generation/receipt. If an outcome is uncertain, poll or retry this same job; do not create a new paid job blindly.
- Quotes expire. Changed project/media facts may require a fresh quote. Confirmation starts generation; it does not approve an ad, publish a post or launch a campaign.

Example request:

```json
{
  "maxCredits": 2
}
```


Example 202 response (selected fields):

```json
{
  "id": "40000000-0000-4000-8000-000000000004",
  "kind": "image",
  "status": "queued",
  "credits": 2,
  "resultUrl": null,
  "resultUrlExpiresIn": null,
  "error": null
}
```


## GET /api/v1/generations/{id}

Read a generation job

Permission: generations:read
Credits: No additional generation credits

Poll the same job about every 10 seconds. A ready result can include a private download URL valid for 900 seconds (15 minutes).

- Possible lifecycle states include preparing, quoted, confirmed, queued, running, ready and failed; the underlying media job can supply its own render state. Treat only ready with a non-null resultUrl as downloadable.
- An expired result URL can be refreshed with another GET. Keep URLs private.
- For failed jobs, check error and the credit receipt in Billing. A callback is optional; polling remains available if callback delivery fails.

Example 200 response (selected fields):

```json
{
  "id": "40000000-0000-4000-8000-000000000004",
  "kind": "image",
  "status": "ready",
  "credits": 2,
  "resultUrl": "https://storage.example.invalid/private-signed-result",
  "resultUrlExpiresIn": 900,
  "error": null
}
```


## POST /api/v1/events

Submit app or website events

Permission: events:write
Credits: No generation credits

Send real first_open, onboarding_done, paywall_view, trial_start and purchase events from a trusted backend for the selected project.

- Send 1–100 events per batch. platform is ios, android or web. appVersion is optional, up to 64 characters.
- Use random UUID installId and dedupeKey values. Reuse a dedupeKey only for the identical event; conflicting reuse rejects the whole batch with 409.
- occurredAt must be an ISO 8601 timestamp within the past 90 days (up to five minutes of future clock skew is tolerated). Replace the example timestamp with the real occurrence time.
- Do not send names, email addresses, hardware/ad identifiers or free-text properties. Never embed this workspace key in a browser or mobile build.
- These are reported events, not independently verified purchases or automatically attributed ad conversions. Do not submit synthetic purchase events to production.

Example request:

```json
{
  "projectId": "10000000-0000-4000-8000-000000000001",
  "events": [
    {
      "name": "first_open",
      "platform": "ios",
      "installId": "30000000-0000-4000-8000-000000000003",
      "dedupeKey": "20000000-0000-4000-8000-000000000002",
      "occurredAt": "2026-11-04T12:00:00Z",
      "appVersion": "1.0"
    }
  ]
}
```


Example 200 response (selected fields):

```json
{
  "accepted": 1,
  "duplicates": 0
}
```


## Narration and video inputs

Narration requires an owned creativeId and exact current version, script 3–960 characters, up to eight nonempty lines of 120 characters each (no angle-bracket markup), voice (Kore, Puck, Zephyr, Charon, Orus, Schedar, Erinome, Despina, Rasalgethi, Iapetus, Algieba, Sadaltager, Gacrux, Achird, Sulafat), and style clear/warm/energetic.

```json
{
  "projectSlug": "your-project",
  "requestKey": "20000000-0000-4000-8000-000000000002",
  "kind": "narration",
  "input": {
    "creativeId": "30000000-0000-4000-8000-000000000003",
    "version": 1,
    "script": "Keep your project notes together.",
    "voice": "Kore",
    "style": "clear"
  }
}
```

Video uses owned photo/recording mediaAssetId or an approved screenshot URL. Required input fields: source, aspect, feed, headline, primaryText, cta, showLogo, cards. Optional presentation, controls, captions, photoIds, screenshotUrls, parentCreativeId/parentRenderVersion, aiGenerated and customer narration are in the OpenAPI schemas. Captions must be ordered/non-overlapping, fit the video, and have positive durations. Narration references a customer trackId, exact script and optional timing; private Lab audio is refused.

```json
{
  "projectSlug": "your-project",
  "requestKey": "20000000-0000-4000-8000-000000000002",
  "kind": "video",
  "input": {
    "source": {
      "type": "photo",
      "mediaAssetId": "30000000-0000-4000-8000-000000000003"
    },
    "aspect": "9:16",
    "feed": "light",
    "headline": "Your product in use",
    "primaryText": "Show one confirmed product benefit.",
    "cta": "Learn more",
    "showLogo": false,
    "cards": []
  }
}
```


## Generation callbacks

Include optional public HTTPS webhookUrl (port 443) in a generation quote. Store returned job-specific webhookSecret server-side. Callback JSON: eventId,type,jobId,kind,status; type is generation.ready or generation.failed. No media URL is included; GET the job for a fresh URL. Verify Meyer-Signature as hex HMAC-SHA256 using webhookSecret over Meyer-Timestamp + "." + exact raw body. Use the returned secret string as the UTF-8 HMAC key without hex-decoding it. Use constant-time comparison, accept only timestamps within five minutes, deduplicate Meyer-Event-Id, and return 2xx after recording. The queue checks every five minutes and makes up to six attempts with backoff. Polling remains available. Redirects/private networks refused; key revocation/expiry stops callbacks. Server signing-key rotation invalidates existing callback secrets.

## Errors

Most errors return {"error":"message"}; agent refusals may also include code, and streaming errors arrive as NDJSON error events.

- 400: Invalid JSON or fields. Correct the request. Retrying unchanged invalid input will not help.
- 401: Missing, expired, revoked or insufficiently scoped key. Check workspace, permissions and expiry; replace or revoke keys in Settings.
- 402: Insufficient credits or agent cap reached. Review Billing and the agent cap before retrying.
- 403: Access refused. Check the issuing owner’s current access and workspace policy.
- 404: Project, task, conversation or job unavailable. Check the ID, workspace and issuing key.
- 409: Conflicting retry, expired quote, cap or unavailable state. Read the error. Reuse an existing request/job where appropriate; avoid duplicate paid work.
- 413: Request too large. Reduce the payload. Limits differ by endpoint.
- 422: Task cannot be prepared. Choose a task with preparable: true or complete it in the workspace.
- 429: Per-key rate limit reached. Honor Retry-After (normally 60 seconds), then retry with backoff.
- 500 / 503: Temporary server or provider/configuration issue. Use bounded backoff. Check existing write/job state before creating another request.

## Claude Code handoff

```text
Connect to the Meyer Launch API using MEYER_LAUNCH_API_KEY from my server environment. Never print, commit or expose the key in client code or URLs.
Read https://meyerlaunch.com/help/api and its OpenAPI document at https://meyerlaunch.com/help/api/openapi.json.
Start read-only with GET https://meyerlaunch.com/api/v1/projects and help me choose a project. Use only permissions granted to the key.
Ask before creating plans, preparing tasks, sending events, messaging the agent or confirming media quotes. Show fixed or quoted credit costs before spending; explain variable agent charges and the workspace cap before requesting approval. Agent calls spend variable credits without a quote; task preparation costs 1 workspace credit; media uses quote then confirmation.
Reuse requestKey and identical input for retries. Respect 429/Retry-After. Do not assume undocumented endpoints exist. Do not send synthetic purchase events.
Keep generated work as drafts for review. Report successful checks and missing permissions; do not claim publication, campaign delivery or paid conversions from a successful API response.
```

