Outwink Public API

v1.0.0

A small REST API over the Outwink Postgres. Manage lists, add leads to meme campaigns, export results as paginated CSV. One header, one curl per call — no SDK required.

On this page
Copy these docs as Markdown to paste into AI tools — includes example responses, errors, and curl for every endpoint.
Treat your API key like a password. Never embed it in browser code in production — call this API from a server you control.

Overview

The Outwink Public API exposes a small set of endpoints over Supabase Postgres. Adding leads costs 1 credit per lead; everything else is free. There is no rate limiting — your credit balance is the only throttle.

This page auto-detects its own origin via window.location.origin, so every curl example uses the URL you loaded the page from. Same payload works on localhost, Railway previews, and any custom domain.

Authentication

Every /v1/* request must include your API key in the X-API-Key header:

X-API-Key: <your-api-key>

The key is a UUID. Get it from the Outwink Settings panel, or rotate it programmatically via POST /v1/auth/rotate-key.

  • No header → 401 missing_authorization.
  • Header present but key doesn't resolve → 401 invalid_api_key.

Errors

Every non-2xx response uses the same flat envelope:

{
  "error": "snake_case_code",
  "message": "Human-readable explanation.",
  "details": { "optional": "context" }
}
StatusCodeWhen
400invalid_inputzod validation failure or malformed JSON.
400list_type_not_supportedEndpoint only accepts certain list types.
401missing_authorizationNo X-API-Key header.
401invalid_api_keyKey didn't resolve.
402insufficient_creditsdetails: { cost, available }.
404list_not_foundList doesn't exist or isn't owned by you.
405method_not_allowedWrong verb on a known path.
500internal_errorAnything unexpected.

Pagination

GET /v1/lists takes page (default 1) and size (default 50, max 200). The response includes a pagination object: { page, size, total, last_page, has_next, has_prev }.

GET /v1/lists/:id/csv takes the same page/size (default 10000, max 10000) and emits these response headers so generic CSV importers can walk pages without parsing the body:

  • X-Total-Count, X-Page, X-Page-Size, X-Last-Page
  • Link per RFC 5988 with rel="first", rel="last", plus rel="next" / rel="prev" when applicable.

Endpoints

GET Get account info
/v1/meAccount

Returns your user record (id, email, full name, current credit balance) plus your most relevant subscription. subscription is null when you have no row in subscriptions.

Request

  
Example response 200 OK application/json
{
  "user_id": "b1ee5f3a-21a9-44f7-9c0d-2cdda8efb0b3",
  "email": "alex@example.com",
  "full_name": "Alex Cohen",
  "credits": 4231,
  "subscription": {
    "plan_name": "growth",
    "status": "active",
    "current_period_end": "2026-06-01T00:00:00Z",
    "cancel_at_period_end": false
  }
}
Errors (3)
StatusCodeWhen
401missing_authorizationNo `X-API-Key` header.
401invalid_api_keyThe key didn't resolve to a user.
500internal_errorAnything unexpected; logged server-side.
Live response
Click Try it to send the request.
GET List all your lists
/v1/listsLists

Paginated summary of every list you own, newest first. Each row includes a lead_count. Use page and size to walk the collection; the response includes a pagination object so you don't have to compute it.

Request

  
Example response 200 OK application/json
{
  "lists": [
    {
      "list_id": "cf9a8f55-7e7c-4d2e-aa1f-4d9b7e25c1a1",
      "list_name": "Q2 Campaign",
      "list_type": "meme",
      "status": "completed",
      "created_at": "2026-04-15T11:23:42Z",
      "lead_count": 142
    },
    {
      "list_id": "2c4f1c33-b8e8-4f5e-9a2c-3f6b2b3a7d9b",
      "list_name": "Welcome funnel",
      "list_type": "meme",
      "status": "completed",
      "created_at": "2026-03-02T08:11:09Z",
      "lead_count": 38
    }
  ],
  "pagination": {
    "page": 1,
    "size": 50,
    "total": 2,
    "last_page": 1,
    "has_next": false,
    "has_prev": false
  }
}
Errors (4)
StatusCodeWhen
400invalid_input`page < 1` or `size` not in 1..200.
401missing_authorizationNo `X-API-Key` header.
401invalid_api_keyThe key didn't resolve to a user.
500internal_errorAnything unexpected; logged server-side.
Live response
Click Try it to send the request.
GET Get list details
/v1/lists/:list_idLists

Returns one list, including the template's variables array (in declared order) and the image_extension (gif if render_spec.has_gif === true, otherwise png). Use these when constructing the body for Generate image.

Request

  
Example response 200 OK application/json
{
  "list_id": "cf9a8f55-7e7c-4d2e-aa1f-4d9b7e25c1a1",
  "list_name": "Q2 Campaign",
  "list_type": "meme",
  "status": "completed",
  "created_at": "2026-04-15T11:23:42Z",
  "lead_count": 142,
  "variables": [
    "first_name",
    "company"
  ],
  "image_extension": "png"
}
Errors (4)
StatusCodeWhen
404list_not_foundList doesn't exist or isn't owned by you.
401missing_authorizationNo `X-API-Key` header.
401invalid_api_keyThe key didn't resolve to a user.
500internal_errorAnything unexpected; logged server-side.
Live response
Click Try it to send the request.
PATCH Rename a list
/v1/lists/:list_idLists

Renames the list. The new name is trimmed and must be 1–120 characters; otherwise the call fails with 400 invalid_input. Nothing else (type, render spec, leads) is touched.

Request

  
Example response 200 OK application/json
{
  "list_id": "cf9a8f55-7e7c-4d2e-aa1f-4d9b7e25c1a1",
  "list_name": "Q2 Campaign"
}
Errors (5)
StatusCodeWhen
400invalid_input`name` is empty or longer than 120 chars after trim.
404list_not_foundList doesn't exist or isn't owned by you.
401missing_authorizationNo `X-API-Key` header.
401invalid_api_keyThe key didn't resolve to a user.
500internal_errorAnything unexpected; logged server-side.
Live response
Click Try it to send the request.
DELETE Delete a list
/v1/lists/:list_idLists

Removes the list and every lead it owns. Ownership is verified up-front so non-owners get 404 list_not_found just like callers asking for a non-existent UUID.

Request

  
Example response 200 OK application/json
{
  "deleted": true,
  "list_id": "cf9a8f55-7e7c-4d2e-aa1f-4d9b7e25c1a1"
}
Errors (4)
StatusCodeWhen
404list_not_foundList doesn't exist or isn't owned by you.
401missing_authorizationNo `X-API-Key` header.
401invalid_api_keyThe key didn't resolve to a user.
500internal_errorAnything unexpected; logged server-side.
Live response
Click Try it to send the request.
POST Generate image (add lead)
/v1/lists/:list_id/leadsLeads

Adds one lead to a meme list and renders its personalised image. Costs 1 credit. variables is a flat object whose keys are the template variable names. Missing keys are not an error — they're inserted as null and reported back in missing_variables; extra keys land in ignored_variables. AI and ice-breaker lists are rejected with 400 list_type_not_supported.

Request

  
Example response 200 OK application/json
{
  "lead_id": "7c2e1b9a-3f4d-4a1c-b7e8-2f3d5c6a8b91",
  "tracking_id": "naia9c7d2f5b81e6c4",
  "image_url": "https://image.outwink.xyz/naia9c7d2f5b81e6c4.png",
  "variables_used": {
    "first_name": "Sam",
    "company": "Acme"
  },
  "missing_variables": [],
  "ignored_variables": [],
  "credits_remaining": 4230
}
Errors (7)
StatusCodeWhen
400invalid_input`variables` is not an object.
400list_type_not_supportedList is `ai` or `ice_breaker`; only `meme` is supported here.
402insufficient_credits`details: { cost, available }`. Top up and retry.
404list_not_foundList doesn't exist or isn't owned by you.
401missing_authorizationNo `X-API-Key` header.
401invalid_api_keyThe key didn't resolve to a user.
500internal_errorAnything unexpected; logged server-side.
Live response
Click Try it to send the request.
GET Export leads as CSV
/v1/lists/:list_id/csvLeads

Streams the leads in a list as a paginated CSV. The first column is tracking_id, the second is image_url, and the remaining columns are the template variables in declared order. The response also sets pagination headers and an RFC 5988 Link header so generic CSV importers can walk the pages without parsing the body. No credits are charged. size is capped at 10000.

Request

  
Example response 200 OK text/csv; charset=utf-8
X-Total-Count: 142 X-Page: 1 X-Page-Size: 10000 X-Last-Page: 1 Link: <{{base}}/v1/lists/{{list_id}}/csv?page=1&size=10000>; rel="first", <{{base}}/v1/lists/{{list_id}}/csv?page=1&size=10000>; rel="last"
tracking_id,image_url,first_name,company
naia9c7d2f5b81e6c4,https://image.outwink.xyz/naia9c7d2f5b81e6c4.png,Sam,Acme
naib0e8c4d2f5b81a3,https://image.outwink.xyz/naib0e8c4d2f5b81a3.png,Jamie,Globex
naic1f9d5e3a6c92b4,https://image.outwink.xyz/naic1f9d5e3a6c92b4.png,Pat,Initech
Errors (5)
StatusCodeWhen
400invalid_input`page < 1` or `size` not in 1..10000.
404list_not_foundList doesn't exist or isn't owned by you.
401missing_authorizationNo `X-API-Key` header.
401invalid_api_keyThe key didn't resolve to a user.
500internal_errorAnything unexpected; logged server-side.
Live response
Click Try it to send the request.
POST Rotate API key
/v1/auth/rotate-keyAuthentication

Generates a fresh API key and overwrites the old one in place. The old key stops authenticating immediately — no grace period — so update any stored credentials before invoking this. Authenticate with the current key.

Request

  
Example response 200 OK application/json
{
  "api_key": "8d1d20c0-5b7e-4d2c-8a3f-0e8b6f1c3a02",
  "rotated_at": "2026-05-10T19:00:00Z"
}
Errors (3)
StatusCodeWhen
401missing_authorizationNo `X-API-Key` header.
401invalid_api_keyThe key didn't resolve to a user.
500internal_errorAnything unexpected; logged server-side.
Live response
Click Try it to send the request.