Skip to content

Docs / WallaB Developer Platform

API reference (v1)

The complete v1 reference: base URL, versioning, rate limits, errors, pagination, and every endpoint.

The complete reference for the WallaB Developer Platform v1 — a read-only, key-authenticated data API for your own systems and AI agents. New here? Start with the overview and quickstart, then read authentication for keys and scopes.

Base URL & versioning

All endpoints live under /api/external/v1/. The absolute base URL is:

https://wallab.ai/api/external/v1

The version is in the path (/v1). We add fields and endpoints without a version bump — your client must ignore unknown JSON fields and not assume a response has only the fields it knows. A breaking change would ship as a new version (/v2) alongside /v1.

Deprecation policy. If a version is ever retired, we announce it in advance and, during the wind-down, send a Deprecation header and a Sunset header (an HTTP-date after which the version stops responding) on every response, per RFC 8594. There is no sunset scheduled for v1.

The unversioned /api/external/entitlements path is kept as a permanent back-compat alias of /v1/entitlements; new integrations should use /v1.

Authentication

Send your key as a bearer token:

Authorization: Bearer wlb_your_key_here

The platform is available on the Growth plan or higher. On Free or Starter every request is refused with 403 { "success": false, "message": "Developer API access requires the Growth plan or higher." }. Any missing, malformed, unknown, or revoked key gets the identical 401 { "success": false, "message": "Unauthorized" } — the API never reveals which check failed. Full detail on keys, scopes, and rotation is on the Authentication page.

Rate limits

Requests are limited per API key, on a rolling one-minute window, by plan:

  • Growth — 60 requests / minute.
  • Pro — 300 requests / minute.
  • Enterprise Plus — uncapped.

Over the limit returns 429 with a Retry-After header (seconds to wait). Design your integration to cache results briefly and back off on Retry-After rather than hammering a retry loop.

Scopes

Each key carries read-only scopes; each endpoint requires exactly one. A key that lacks the required scope is refused with 403.

  • read:entitlements — customer entitlement status.
  • read:subscriptions — subscription records and detail.
  • read:metrics — aggregate retention/revenue metrics and cancel-save outcomes.
  • read:plans — selling-plan definitions.

Errors

Every non-2xx response is the same envelope:

{ "success": false, "message": "A human-readable explanation." }

Status codes:

  • 400 Bad Request — invalid input (a bad customerId, limit, cursor, or filter value).
  • 401 Unauthorized — any authentication failure (uniform body; the reason is never distinguishable).
  • 403 Forbidden — the plan lacks developer API access, or the key is missing the endpoint's required scope.
  • 404 Not Found — the subscription-detail endpoint only, for an id that isn't yours (identical to a missing id — no existence oracle).
  • 429 Too Many Requests — rate limited; wait the Retry-After seconds.
  • 500 Internal Server Error — something went wrong on our side; safe to retry later.

Pagination

List endpoints (/subscriptions and /cancellations) are cursor-paginated:

  • limit — page size, 1–100, default 50.
  • cursor — pass the previous response's nextCursor to fetch the next page. It is opaque; treat it as a token, not a value to construct.
  • Each response carries a nextCursor — a string when more rows remain, or null on the last page. An invalid cursor or limit returns 400.

Machine-readable specs

Point your tooling and AI agents at the generated contract — it is built from the same source as this page, so it can never drift from the live endpoints:

Every endpoint below is also exposed as a read-only tool on the MCP server, for AI assistants that speak the Model Context Protocol.

GET /api/external/v1/subscriptions

Scope: read:subscriptions. List the shop's subscriptions.

Query params: limit, cursor (see pagination), and an optional status filter — one of active, paused, cancelled, expired, failed.

curl -sS "https://wallab.ai/api/external/v1/subscriptions?limit=50&status=active" \
  -H "Authorization: Bearer wlb_your_key_here"
const url = new URL("https://wallab.ai/api/external/v1/subscriptions");
url.searchParams.set("limit", "50");
url.searchParams.set("status", "active");
const res = await fetch(url, {
  headers: { Authorization: "Bearer wlb_your_key_here" },
});
const { subscriptions, nextCursor } = await res.json();
{
  "subscriptions": [
    {
      "subscriptionId": 501,
      "customerId": 1042,
      "status": "active",
      "planId": 7,
      "planName": "Coffee Club Monthly",
      "planType": "physical",
      "currentPrice": 24.50,
      "nextBillingDate": "2026-08-01T00:00:00.000Z",
      "createdAt": "2026-01-01T00:00:00.000Z"
    }
  ],
  "nextCursor": "NTAx"
}

currentPrice is the subscription's per-cycle value (line prices × quantity). customerId is an opaque, shop-scoped internal id — never PII.

Errors: 400 (bad limit, cursor, or status), 401, 403, 429.

GET /api/external/v1/subscriptions/{id}

Scope: read:subscriptions. One subscription's detail plus its line items.

{id} is the numeric subscription id. A missing, non-numeric, or another shop's id all return the identical 404 { "success": false, "message": "Subscription not found." } — the endpoint can't be used to probe which ids exist. Line items carry the shop's own catalog fields (product/variant ids and titles) — that's your data, not customer data.

curl -sS "https://wallab.ai/api/external/v1/subscriptions/501" \
  -H "Authorization: Bearer wlb_your_key_here"
const res = await fetch(
  "https://wallab.ai/api/external/v1/subscriptions/501",
  { headers: { Authorization: "Bearer wlb_your_key_here" } },
);
if (res.status === 404) throw new Error("No such subscription");
const { subscription } = await res.json();
{
  "subscription": {
    "subscriptionId": 501,
    "customerId": 1042,
    "status": "active",
    "planId": 7,
    "planName": "Coffee Club Monthly",
    "planType": "physical",
    "currentPrice": 24.50,
    "nextBillingDate": "2026-08-01T00:00:00.000Z",
    "createdAt": "2026-01-01T00:00:00.000Z",
    "lineItems": [
      {
        "lineId": 9,
        "productId": 100,
        "productTitle": "Dark Roast",
        "variantId": 200,
        "variantTitle": "12 oz",
        "quantity": 2,
        "currentPrice": 12.50
      }
    ]
  }
}

Errors: 401, 403, 404 (unknown/foreign/malformed id), 429.

GET /api/external/v1/metrics/summary

Scope: read:metrics. The dashboard KPIs, programmatically — the exact same figures the admin dashboard shows (same engine, so they can never disagree). Aggregate numbers only; no per-customer rows.

curl -sS "https://wallab.ai/api/external/v1/metrics/summary" \
  -H "Authorization: Bearer wlb_your_key_here"
const res = await fetch(
  "https://wallab.ai/api/external/v1/metrics/summary",
  { headers: { Authorization: "Bearer wlb_your_key_here" } },
);
const { summary } = await res.json();
{
  "summary": {
    "activeSubscribers": 128,
    "pausedSubscribers": 12,
    "mrr": 3200.00,
    "revenue30d": 2980.00,
    "newSubscribers30d": 18,
    "cancelled30d": 4,
    "churnRate30d": 0.031,
    "saveRate": 0.62,
    "cancelSessions30d": 21,
    "dunningActive": 3,
    "recoveryRate": 0.75,
    "failedPayments30d": 2,
    "avgSubscriptionValue": 25.00,
    "upcomingRenewals7d": 30,
    "upcomingRenewalsValue7d": 740.00,
    "generatedAt": "2026-07-08T00:00:00.000Z"
  }
}

Money values are in the shop's currency; rates (churnRate30d, saveRate, recoveryRate) are fractions in [0, 1].

Errors: 401, 403, 429.

GET /api/external/v1/plans

Scope: read:plans. The shop's selling plans (your subscribe-and-save cadences), with a pricing summary, active flag, and product count. Returns the full set (plans per shop are bounded — no pagination).

curl -sS "https://wallab.ai/api/external/v1/plans" \
  -H "Authorization: Bearer wlb_your_key_here"
const res = await fetch("https://wallab.ai/api/external/v1/plans", {
  headers: { Authorization: "Bearer wlb_your_key_here" },
});
const { plans } = await res.json();
{
  "plans": [
    {
      "id": 7,
      "name": "Monthly",
      "groupName": "Coffee Club",
      "intervalUnit": "month",
      "intervalCount": 1,
      "active": true,
      "productCount": 4,
      "pricing": {
        "discountType": "percentage",
        "discountValue": 15,
        "summary": "15% off"
      }
    }
  ]
}

Errors: 401, 403, 429.

GET /api/external/v1/cancellations

Scope: read:metrics (cancel/save outcomes are the same retention analytics that scope already governs — no separate scope to grant). The cancel-save concierge outcomes, cursor-paginated and opaque (no customer PII).

Query params: limit, cursor (see pagination), and an optional outcome filter — one of saved, cancelled, abandoned.

curl -sS "https://wallab.ai/api/external/v1/cancellations?limit=50&outcome=saved" \
  -H "Authorization: Bearer wlb_your_key_here"
const url = new URL("https://wallab.ai/api/external/v1/cancellations");
url.searchParams.set("limit", "50");
url.searchParams.set("outcome", "saved");
const res = await fetch(url, {
  headers: { Authorization: "Bearer wlb_your_key_here" },
});
const { cancellations, nextCursor } = await res.json();
{
  "cancellations": [
    {
      "sessionId": 88,
      "detectedReason": "too expensive",
      "outcome": "saved",
      "offerType": "offer_discount",
      "createdAt": "2026-06-01T00:00:00.000Z"
    }
  ],
  "nextCursor": null
}

outcome is saved, cancelled, abandoned, or null for an in-progress session; offerType is the concierge offer the shopper was shown (offer_discount, pause, swap, skip_next) or null if none.

Errors: 400 (bad limit, cursor, or outcome), 401, 403, 429.