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
{
"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
{
"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_urlis the billing portal's cancel page; cancelling keeps Agency until the period ends. - Your current plan: nothing changes and
redirect_urlisnull.
Agency returns 400 parameter_invalid: email sales@spysay.com. Without billing set up on the server, this returns 503 service_unavailable.
curl -X POST "https://spysay.com/v1/subscription" \
-H "x-spysay-api-key: <API_KEY>" \
-H "content-type: application/json" \
-d '{ "plan": "growth" }'
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
{
"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.
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" }'
{
"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
}
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 |
{
"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) |