Billing API

Spysay plans, the subscription, and the credits that pay for checks, renders and audits: balances, packs, purchases and history.

A plan sets how many markets and questions you can track. Growth is pay as you go: no monthly fee and no included credits. Agency is a subscription that includes credits every calendar month (UTC). Work that costs something spends credits: first the plan's, then any you bought. Reading data is always free.

Uses credits Free
Each AI answer checked Reading reports, prompts, runs and stats
Each page an audit crawls Analytics
Each fresh render, and each page a cache purge re-renders Every endpoint on this page

When the balance can't cover new work, the request fails with 402 insufficient_credits and nothing is charged. Scheduled work reserves its credits before it starts, so a check never stops halfway.

Plans

GET /v1/plans

json
{
  "object": "list",
  "data": [
    { "id": "growth", "object": "plan", "name": "Growth", "price": 0, "limits": { "markets": 5, "prompts": 100 }, "credits": 0 },
    { "id": "agency", "object": "plan", "name": "Agency", "price": null, "limits": { "markets": 25, "prompts": 1000 }, "credits": 40000 }
  ],
  "has_more": false,
  "next_cursor": null
}

price is USD per month: 0 is pay as you go, null means the plan is set up with our team.

The subscription

GET /v1/subscription

json
{
  "id": "sub_1QxYz2LkdIwHu7ix",
  "object": "subscription",
  "plan": "agency",
  "status": "active",
  "current_period_start": 1790812800,
  "current_period_end": 1793491200,
  "pending_plan": null,
  "usage": { "markets": 4, "prompts": 84 },
  "next_invoice": null,
  "payment_method": null
}
Field Meaning
id The Stripe subscription, or sub_free on Growth
current_period_start, current_period_end The calendar month (UTC) your credits are counted in
usage What you track now, against the plan's limits
next_invoice null on Growth
payment_method null: the card is in the billing portal

POST /v1/subscription

Parameter Type Description
plan (required) string growth or agency

Payment happens at Stripe, so a change is finished by a person at redirect_url, and the response still shows the plan you're on now:

  • On Agency, choosing growth: redirect_url is the billing portal's cancel page; cancelling keeps Agency until the period ends.
  • Your current plan: nothing changes and redirect_url is null.

Agency returns 400 parameter_invalid: email sales@spysay.com. Without billing set up on the server, this returns 503 service_unavailable.

sh
curl -X POST "https://spysay.com/v1/subscription" \
  -H "x-spysay-api-key: <API_KEY>" \
  -H "content-type: application/json" \
  -d '{ "plan": "growth" }'
ts
import { createClient } from "@spysay/sdk";

const spysay = createClient({ baseUrl: "https://spysay.com", token: process.env.SPYSAY_API_KEY! });

const change = await spysay.subscription.update({ plan: "growth" });
if (change.redirectUrl) console.log(`Finish at ${change.redirectUrl}`);

Credits

GET /v1/credit_balance

json
{
  "object": "credit_balance",
  "balance": 600,
  "included": 3500,
  "used_this_period": 2900,
  "period_end": 1793491200
}

GET /v1/credit_packs

Top-ups: pack_1000 (1,000 credits, $20), pack_5000 (5,000, $90) and pack_20000 (20,000, $320).

POST /v1/credit_purchases

Parameter Type Description
pack (required) string A pack ID from GET /v1/credit_packs

Starts a purchase and returns 201 with it, pending, and a checkout_url: a Stripe Checkout page where a person pays. The credits are added as soon as the payment goes through, and show in GET /v1/credit_transactions as a purchase. A Checkout link expires after 24 hours; an unpaid purchase adds nothing.

Send an Idempotency-Key header (optional, as in Stripe): any unique string up to 255 characters, such as a UUID per purchase. A retry with the same key within 24 hours returns the same purchase and the same checkout_url, so a request that timed out can be sent again safely. Reusing a key for a different pack returns 400 parameter_invalid.

sh
curl -X POST "https://spysay.com/v1/credit_purchases" \
  -H "x-spysay-api-key: <API_KEY>" \
  -H "Idempotency-Key: 6f1c2b7e-buy-5000" \
  -H "content-type: application/json" \
  -d '{ "pack": "pack_5000" }'
json
{
  "id": "cs_a1Bc2De3Fg4Hi5Jk",
  "object": "credit_purchase",
  "pack": "pack_5000",
  "credits": 5000,
  "amount": 90,
  "status": "pending",
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_a1Bc2De3Fg4Hi5Jk",
  "created_at": 1792915200
}
ts
import { createClient } from "@spysay/sdk";

const spysay = createClient({ baseUrl: "https://spysay.com", token: process.env.SPYSAY_API_KEY! });

const balance = await spysay.creditBalance.retrieve();
if (balance.balance < 1000) {
  const purchase = await spysay.creditPurchases.create({ pack: "pack_5000" });
  console.log(`Pay at ${purchase.checkoutUrl}`);
}

GET /v1/credit_transactions

Every movement on the balance, newest first. amount is signed: grants and purchases add, usage subtracts.

Parameter Type Description
type string plan_grant, purchase, usage, refund or bonus (free credits for verifying a business, never refunded)
limit, starting_after Pagination
json
{
  "id": "ctx_w4run",
  "object": "credit_transaction",
  "type": "usage",
  "amount": -600,
  "operation": "prompt_run",
  "description": "Weekly answer checks, 4 markets",
  "created_at": 1792742400
}

Invoices and payment method

POST /v1/billing_portal_sessions

Parameter Type Description
return_url (required) string Where the portal sends you back: an https URL on your spysay app or spysay.com. Anything else returns 400 parameter_invalid.

Returns a short-lived url to the Stripe billing portal, where you update the card, download past invoices and cancel. Without billing set up on the server, this returns 503 service_unavailable.

Status Codes
400 parameter_invalid
402 insufficient_credits (on billable endpoints)
404 resource_missing
503 service_unavailable (billing isn't set up on this server, or Stripe didn't answer)