Skip to content
Wahlu API

Discovery & Readiness

Discover accessible brands, publishing targets, platform capabilities, and content readiness.

Use these read operations and one explicit provider-backed refresh to discover the caller's exact authority, read a brand's voice and labels, choose a connected target, retrieve its live TikTok privacy choices when required, and inspect Wahlu's public platform rules. Follow the resolved links returned by the API instead of constructing brand routes yourself.

Get plans and limits

GET/v1/plans

Public and read-only. No key or scope is needed. Returns current catalogue prices in USD, billing periods, credit allocations and limits. Social accounts count across the workspace. Network and media caps apply within a brand. The response uses the usual success, data and meta envelope.

curl
curl https://api.wahlu.com/v1/plans

The matching local stdio MCP tool is get_plans and needs no extra key scopes. The hosted connector excludes catalogue discovery. Plain-text prices are at pricing.md.

Plan-limit errors keep the usual error envelope. Their details include limit, used, max and upgrade_url. Local API clients can use that link when needed. Check the catalogue before suggesting a plan, since some caps stay the same on higher plans. Hosted directory tools return neutral access guidance without upgrade links.

Get agent context

GET/v1/context

Requires a valid API key but no additional scope. It returns the actor, workspace, the key's exact scopes and brand restrictions, accessible brands, and resolved links. Credentials and provider secrets are never included.

The default response keeps the original public scope list for older clients. Add?scope_disclosure=effective to also see already-held History and notification read permissions. This option grants no access and excludes sensitive or administrative scopes. Current CLI and MCP clients request it explicitly.

curl
curl https://api.wahlu.com/v1/context \
  -H "Authorization: Bearer wahlu_live_your_api_key_here"
200 response
{
  "success": true,
  "data": {
    "actor": { "user_id": "user_01k0agent" },
    "workspace": { "id": "workspace_01k0acme", "name": "Acme Social" },
    "api_key": {
      "id": "api_key_01k0agent",
      "name": "Content agent",
      "scopes": [
        "brands:read",
        "integrations:read",
        "media:read",
        "media:write",
        "posts:read",
        "posts:write",
        "schedule:read",
        "schedule:write"
      ],
      "brand_access": "restricted",
      "brand_ids": ["brand_01k0acme"]
    },
    "brands": [
      {
        "id": "brand_01k0acme",
        "name": "Acme",
        "links": {
          "targets": {
            "href": "/v1/brands/brand_01k0acme/targets",
            "required_scopes": ["integrations:read"]
          },
          "media_imports": {
            "href": "/v1/brands/brand_01k0acme/media/imports",
            "method": "POST",
            "required_scopes": ["media:write"]
          },
          "content_items": {
            "href": "/v1/brands/brand_01k0acme/content-items",
            "method": "POST",
            "required_scopes": ["posts:write"]
          }
        }
      }
    ],
    "links": {
      "platform_capabilities": {
        "href": "/v1/platforms/capabilities",
        "required_scopes": []
      }
    }
  },
  "meta": { "request_id": "req_context_01k0" }
}

Get brand context

GET/v1/brands/:brand_id/context

Requires posts:read. Read this before writing captions so drafts sound like the brand. It returns the brand's description, website, category and timezone; its logo, brand kit colours and fonts; the owner's custom AI instructions; the brand profile and content strategy, which cover voice, audience and content pillars as Markdown; the default call to action; and the Link in bio page status, with its public URL while the page is published. It is read-only and never changes the brand.

curl
curl https://api.wahlu.com/v1/brands/brand_01k0acme/context \
  -H "Authorization: Bearer $WAHLU_API_KEY"
200 response
{
  "success": true,
  "data": {
    "brand": {
      "id": "brand_01k0acme",
      "name": "Acme Bakery",
      "description": "Neighbourhood sourdough bakery in Fitzroy.",
      "website": "https://acmebakery.example",
      "category": "restaurant_food",
      "timezone": "Australia/Melbourne"
    },
    "identity": {
      "logo_url": "https://media.wahlu.com/brands/acme/logo.png",
      "logo_light_url": null,
      "logo_dark_url": null,
      "colors": ["#C2410C", "#1C1917", "#FAFAF9"],
      "title_font": "Playfair Display",
      "body_font": "Inter"
    },
    "voice": {
      "custom_instructions": "Always use Australian English.",
      "profile": "## Brand Voice & Personality\nWarm, unhurried and a little cheeky.",
      "strategy": "## Content pillars\n1. Behind the bench\n2. Seasonal bakes",
      "default_call_to_action": { "text": "Order for pick-up", "url": "https://acmebakery.example/order" }
    },
    "link_in_bio": {
      "status": "published",
      "handle": "acmebakery",
      "url": "https://wahlu.me/acmebakery",
      "published_at": "2026-09-20T02:00:00.000Z"
    },
    "links": { "self": { ... }, "labels": { ... }, "targets": { ... } }
  },
  "meta": { "request_id": "req_brand_context_01" }
}

link_in_bio.status is not_created, draft, published or offline (taken down after a report). Any field the brand hasn't filled in is null.

List brand labels

GET/v1/brands/:brand_id/labels

Requires posts:read. Returns every label for the brand, sorted by name, each with its id, name and color. Use the IDs in a content item's label_ids.

curl
curl https://api.wahlu.com/v1/brands/brand_01k0acme/labels \
  -H "Authorization: Bearer $WAHLU_API_KEY"

List brand targets

GET/v1/brands/:brand_id/targets

Requires integrations:read. Use the brand ID from context, or follow that brand's links.targets.href. Each target reports its connection state, schedulable readiness, capabilities, blockers, and bounded repair actions.

A connected target has an integration_id; carry that exact ID into draft preflight and Schedule creation. A disconnected discovery row can have a null integration ID. Provider tokens, provider account IDs, and complete provider profiles are not returned.

curl
curl https://api.wahlu.com/v1/brands/brand_01k0acme/targets \
  -H "Authorization: Bearer wahlu_live_your_api_key_here"
Select a ready target
const target = payload.data.targets.find(
  (candidate) => candidate.schedulable && candidate.integration_id
);

if (!target) {
  const guidance = payload.data.targets.flatMap(
    (candidate) => candidate.repair_actions.map((action) => action.guidance)
  );
  throw new Error(guidance.join(" ") || "No schedulable target is available");
}

const integrationId = target.integration_id;

Get target dynamic options

GET/v1/brands/:brand_id/targets/:integration_id/dynamic-options

Requires integrations:read. Call this only after selecting an authorised TikTok target. Wahlu reads that exact connected account live and returns its currently supported creator_privacy_levels plus bounded comment, duet, and stitch constraints. This compatible read never refreshes credentials, acquires a provider-effect lease, or changes integration status. If the current credential cannot safely support the read, the request fails explicitly.

Never infer or default a privacy level. Present the returned options to the user and carry the selected canonical value into the precise draft privacy update. Reconnection, foreign-target, malformed-provider, and dependency failures return explicit errors instead of stale choices.

Read live TikTok privacy choices
curl https://api.wahlu.com/v1/brands/brand_01k0acme/targets/integration_01k0tiktok/dynamic-options \
  -H "Authorization: Bearer $WAHLU_API_KEY"
200 response
{
  "success": true,
  "data": {
    "brand_id": "brand_01k0acme",
    "integration_id": "integration_01k0tiktok",
    "platform": "tiktok",
    "kind": "creator_privacy_levels",
    "options": [
      { "value": "SELF_ONLY", "label": "Only you" },
      { "value": "MUTUAL_FOLLOW_FRIENDS", "label": "Friends" }
    ],
    "constraints": {
      "comments_disabled": false,
      "duets_disabled": true,
      "stitches_disabled": true
    },
    "retrieved_at": "2026-08-09T02:00:00.000Z",
    "links": {
      "self": {
        "href": "/v1/brands/brand_01k0acme/targets/integration_01k0tiktok/dynamic-options",
        "required_scopes": ["integrations:read"]
      }
    }
  },
  "meta": { "request_id": "req_dynamic_options_01k0" }
}

Refresh target dynamic options

POST/v1/brands/:brand_id/targets/:integration_id/dynamic-options

Requires integrations:write. Use this separate medium-risk action when the safe read reports that current credentials cannot proceed. Its strict body is{} and it accepts no idempotency key. The action may rotate stored credentials, update the provider-effect lease, or mark the integration for reauthorisation. It changes no content, creates no Schedule, submits no provider post, and returns no credential or raw provider payload.

Refresh live TikTok privacy choices
curl -X POST https://api.wahlu.com/v1/brands/brand_01k0acme/targets/integration_01k0tiktok/dynamic-options \
  -H "Authorization: Bearer $WAHLU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Get platform capabilities

GET/v1/platforms/capabilities

This public operation requires no API key. It returns the public platform registry: account modes, post types, media and text rules, settings fields, and agent guidance. Hidden and internal platform records are excluded.

Responses include an ETag and a five-minute public cache policy. Send the ETag in If-None-Match; an unchanged registry returns 304 with no body.

curl
curl https://api.wahlu.com/v1/platforms/capabilities \
  -H 'If-None-Match: "previous-etag"'