gobadger

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 11 Imported by: 0

README

gobadger

A Go client for the ScrapeBadger web-scraping API. Standard library only — no dependencies.

go get github.com/flint-development-llc/gobadger

Usage

client, err := gobadger.New(os.Getenv("SCRAPEBADGER_API_KEY"))
if err != nil {
    return err
}

res, err := client.Scrape(ctx, "https://example.com", gobadger.Request{
    Format: gobadger.FormatMarkdown,
})
if err != nil {
    return err
}
fmt.Println(res.Content)

Request covers the full documented parameter set — engine tier, output format, JS rendering and waits, browser action scenarios, sessions, proxy country, custom headers, retries, screenshots and video, anti-bot solving, escalation, a credit budget, and AI extraction. Every field is documented inline with its default, valid range, and credit cost.

What the client handles for you

Scrape is synchronous: it returns rendered content, or an error naming why it could not. Three upstream behaviours are absorbed rather than pushed onto callers:

  • Rate limits. A 429 is waited out per Retry-After and retried, bounded by RateLimitAttempts and the context.
  • Asynchronous unblocks. When the API answers 202 with a poll URL instead of a render, the client polls it to completion, bounded by PollAttempts and the context. If it doesn't resolve in time you get ErrUnblockPending along with the JobID that was left running.
  • Failures reported as success. The API sometimes answers 200 with success:false and the reason in one of several fields. That becomes a real error, so an empty Content can never be mistaken for a scrape of an empty page.

The context is what should bound a call — a browser-tier render routinely runs for tens of seconds, longer with retries. The client's own HTTP timeout is only a backstop against a hung connection.

Errors

Match with errors.Is:

Error Meaning
ErrBlocked Target still blocking after every retry. Response.BlockingDetails names the system.
ErrInsufficientCredits Account is out of credits.
ErrBadRequest Invalid URL, unavailable engine, or estimated cost above MaxCost.
ErrRateLimited Still rate limited after RateLimitAttempts waits.
ErrUnblockPending Async unblock didn't finish in time. Retrying later may succeed.
ErrUnauthorized API key rejected.
ErrNoAPIKey New called with an empty key.
ErrNotConfigured Method called on a nil *Client.

Where the API returned a usable body the Response comes back alongside the error, so CreditsUsed, BlockingDetails, and RetriesUsed are still available on a failure — worth logging before discarding.

Account balance

acc, err := client.Account(ctx)   // free — deducts no credits
fmt.Println(acc.TotalCreditsBalance)

Not cached. If you poll this for a dashboard, cache it yourself.

Notes on the wire contract

  • Unknown enum values decode cleanly. Engine, Format, and the rest are named string types with no membership check, so a tier or format added upstream won't fail a response. Anything branching on one must handle a value outside the declared constants.
  • Response.EngineUsed is a plain string, not Engine — the tiers that can answer (http, curl_cffi, patchright, windows_chrome, …) are a wider set than the two a request can ask for.
  • RetryCount and RetryOnBlock are pointers because their API defaults are non-zero (3 and true). A plain int/bool with omitempty could never transmit an explicit 0/false.
  • Some behaviour is undocumented but real and handled here: the 202 async unblock envelope, and failures returned with a 200 status.

Configuration

WithBaseURL takes the API origin (https://scrapebadger.com), not a full endpoint path — the client appends /v1/web/scrape and /v1/account/me itself. WithHTTPClient replaces the HTTP client entirely, for custom timeouts, transports, or instrumentation.

Documentation

Overview

Package gobadger is a Go client for the ScrapeBadger web-scraping API (https://scrapebadger.com). It covers page rendering — including the browser tier, AI extraction, and anti-bot handling — and the account balance endpoint.

It has no dependencies outside the standard library.

The client is synchronous: Scrape returns rendered content, or an error naming why it could not. Three upstream behaviours are absorbed rather than pushed onto callers — rate limits are waited out and retried, an asynchronous unblock is polled to completion, and a failure the API reports with a 2xx status is turned into a real error, so an empty result can never be mistaken for an empty page.

A basic scrape:

client, err := gobadger.New(os.Getenv("SCRAPEBADGER_API_KEY"))
if err != nil {
	return err
}
res, err := client.Scrape(ctx, "https://example.com", gobadger.Request{
	Format: gobadger.FormatMarkdown,
})
if err != nil {
	return err
}
fmt.Println(res.Content)

Every call takes a context, and the context is what should bound it — a scrape on the browser tier routinely runs for tens of seconds, and longer still when it retries or waits on an unblock. The client's own HTTP timeout is only a backstop against a hung connection.

Index

Constants

View Source
const (
	// DefaultBaseURL is the root of the hosted ScrapeBadger API. Override it with
	// WithBaseURL to point at a proxy or a test server.
	DefaultBaseURL = "https://scrapebadger.com"

	// DefaultTimeout backstops a single HTTP call. It is deliberately generous —
	// a browser-tier render legitimately takes tens of seconds — because the caller's
	// context, not this, is meant to bound the work. It exists only so a hung
	// connection cannot pin a goroutine forever.
	DefaultTimeout = 5 * time.Minute
)
View Source
const (
	// RateLimitAttempts bounds how many times a call waits out a 429 and retries.
	// The limit is per-minute, so one wait of Retry-After (≤~59s) almost always
	// clears it; a couple more cover several callers retrying into the same window.
	// Each wait respects the context, so this can't tie up a request indefinitely.
	RateLimitAttempts = 3

	// PollAttempts caps how many times an async unblock is polled. With PollInterval
	// that bounds an unblock at roughly two minutes — but the context still wins, so
	// a caller with a shorter deadline gets ErrUnblockPending sooner.
	PollAttempts = 40
)
View Source
const AccountPath = "/v1/account/me"

AccountPath is the API path Account reads, appended to the client's base URL.

View Source
const ScrapePath = "/v1/web/scrape"

ScrapePath is the API path Scrape posts to, appended to the client's base URL.

Variables

View Source
var (
	// ErrNoAPIKey is returned by New when the API key is empty.
	ErrNoAPIKey = errors.New("gobadger: no api key")

	// ErrNotConfigured is returned by a method called on a nil *Client. Callers that
	// keep a nil client to mean "not configured" get an error rather than a panic.
	ErrNotConfigured = errors.New("gobadger: client not configured")

	// ErrBlocked means the target is still blocking after every retry was exhausted
	// (HTTP 422, or a success:false envelope). Response.BlockingDetails names the
	// system that blocked it.
	ErrBlocked = errors.New("gobadger: target blocked the scrape")

	// ErrInsufficientCredits means the account is out of credits (HTTP 402).
	ErrInsufficientCredits = errors.New("gobadger: insufficient credits")

	// ErrBadRequest means the request was rejected (HTTP 400): an invalid URL, an
	// engine that isn't available, or an estimated cost above Request.MaxCost.
	ErrBadRequest = errors.New("gobadger: request rejected")

	// ErrRateLimited means the per-minute rate limit was still in force after the
	// client had waited it out RateLimitAttempts times.
	ErrRateLimited = errors.New("gobadger: rate limited")

	// ErrUnblockPending means an asynchronous unblock was started but had not
	// finished within the context or the client's poll budget. The scrape may well
	// succeed if retried later; Response.JobID identifies the unblock that was left
	// running.
	ErrUnblockPending = errors.New("gobadger: unblock still pending")

	// ErrUnauthorized means the API key was rejected (HTTP 401 or 403).
	ErrUnauthorized = errors.New("gobadger: unauthorized")
)

The errors a caller is likely to want to tell apart — an operator can act on "out of credits" or "the target is blocking us", but not on a generic 500. Match them with errors.Is. Where the API returned a usable body, Scrape returns it alongside the error, so the Response is still worth inspecting (CreditsUsed, BlockingDetails, and so on) even on a failure.

Functions

This section is empty.

Types

type Account

type Account struct {
	CreditsBalance             int           `json:"credits_balance"`
	SubscriptionCreditsBalance int           `json:"subscription_credits_balance"`
	TotalCreditsBalance        int           `json:"total_credits_balance"`
	Tier                       string        `json:"tier"`
	RateLimitPerMinute         int           `json:"rate_limit_per_minute"`
	Subscription               *Subscription `json:"subscription"`
}

Account is the credit balance and plan behind an API key. CreditsBalance is the never-expiring pay-as-you-go balance; SubscriptionCreditsBalance is the current period's allowance; TotalCreditsBalance is their sum, which is what a scrape actually burns against.

type Action

type Action string

Action is the operation a single JSAction performs. It also decides which of that struct's other fields apply — see JSAction.

const (
	// ActionClick clicks an element. Needs Selector.
	ActionClick Action = "click"

	// ActionFill fills an input field. Needs Selector and Value.
	ActionFill Action = "fill"

	// ActionScroll scrolls the page. Needs Direction and Amount.
	ActionScroll Action = "scroll"

	// ActionWait waits for a duration. Needs Milliseconds.
	ActionWait Action = "wait"
)

type BlockingDetails

type BlockingDetails struct {
	// Whether the page is confirmed as a blocking page.
	IsBlocked bool `json:"is_blocked"`

	// Type of block detected (e.g. cloudflare, datadome, akamai, kasada).
	BlockType string `json:"block_type"`

	// Confidence score from 0.0 to 1.0.
	Confidence float64 `json:"confidence"`

	// Human-readable description of the block.
	Details string `json:"details"`
}

BlockingDetails is the nested blocking_details object, populated only when Response.BlockingDetected is set. Details is often the only place the real failure appears on a success:false envelope that leaves Error and Message empty — e.g. "No engines available in override chain", a pre-flight rejection that arrives with IsBlocked false and so is not an anti-bot block at all.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to the ScrapeBadger API. It is safe for concurrent use, holds no per-request state, and does no caching: a caller that polls the account balance frequently should cache the result itself.

func New

func New(apiKey string, opts ...Option) (*Client, error)

New returns a Client authenticating with the given API key. It fails with ErrNoAPIKey if the key is empty, so a missing configuration surfaces at startup rather than as a 401 on the first scrape.

func (*Client) Account

func (c *Client) Account(ctx context.Context) (*Account, error)

Account returns the current credit balance. The call is free — it deducts no credits — but it is not cached: a caller polling it on a dashboard should cache the result itself rather than calling this on every page load.

func (*Client) Scrape

func (c *Client) Scrape(ctx context.Context, url string, req Request) (Response, error)

Scrape renders one page and returns the result. url overrides req.URL, so a caller can keep a reusable options value and pass the target separately.

It absorbs three upstream behaviours rather than pushing them onto the caller: a 429 is waited out and retried (per Retry-After, bounded by RateLimitAttempts); an async-unblock envelope is polled to completion (bounded by PollAttempts and the context); and a failure reported as HTTP 200 with success:false is turned into a real error, so an empty Content can never be mistaken for a scrape of an empty page.

The returned Response is populated on most error paths — a blocked scrape carries full diagnostics, and a failed render still reports CreditsUsed — so it is worth logging or recording before being discarded.

type DetectedSystem

type DetectedSystem struct {
	// System name (e.g. cloudflare_turnstile, datadome, recaptcha_v2, hcaptcha).
	System string `json:"system"`

	// Confidence score from 0.0 to 1.0.
	Confidence float64 `json:"confidence"`

	// Additional detection details.
	Details string `json:"details"`
}

DetectedSystem is one entry of Response.AntiBotSystems or Response.CaptchaSystems — both arrays share this shape. Details is frequently null.

type Engine

type Engine string

Engine is the scraping engine tier a request asks for. Note that several request flags (RenderJS, WaitFor, JSScenario, Screenshot, Video) force the browser tier on their own, so setting any of them means paying browser prices no matter what this says.

const (
	// EngineAuto automatically picks the best engine for the target site
	// (recommended): fast HTTP at 1 credit for simple pages, a browser at 5 for
	// JavaScript-heavy ones. With Escalate it can go further still, to a premium
	// browser at 10 credits, for heavily protected sites.
	EngineAuto Engine = "auto"

	// EngineBrowser forces a headless browser with full JavaScript rendering.
	// Always 5 credits, even for a page the HTTP tier could have handled.
	EngineBrowser Engine = "browser"
)

type Format

type Format string

Format is what ScrapeBadger converts the page into before returning it. Markdown is usually the right choice when the content is headed for an LLM — it keeps the document structure an extractor needs while dropping the markup that dominates a content-heavy page's token count.

const (
	// FormatHTML returns the raw HTML of the page.
	FormatHTML Format = "html"

	// FormatMarkdown returns the page converted to clean Markdown.
	FormatMarkdown Format = "markdown"

	// FormatText returns plain text with the HTML tags stripped.
	FormatText Format = "text"
)

type JSAction

type JSAction struct {
	// The operation this step performs.
	Type Action `json:"type"`

	// CSS selector the step acts on. Required by ActionClick and ActionFill.
	Selector string `json:"selector,omitempty"`

	// Text to type into the selected input. Required by ActionFill.
	Value string `json:"value,omitempty"`

	// Which way to scroll. Required by ActionScroll.
	Direction ScrollDirection `json:"direction,omitempty"`

	// How far to scroll, in pixels. Required by ActionScroll.
	Amount int `json:"amount,omitempty"`

	// How long to pause. Required by ActionWait.
	Milliseconds int `json:"milliseconds,omitempty"`
}

JSAction is one step in a js_scenario, run against the page before the content is captured — which is how a collapsed section gets expanded into the markup the extractor eventually sees. Type selects the operation and decides which of the remaining fields apply; everything but Type is omitempty so a step sends only its own parameters.

type Option

type Option func(*Client)

Option configures a Client. See WithBaseURL and WithHTTPClient.

func WithBaseURL

func WithBaseURL(u string) Option

WithBaseURL overrides the API root (DefaultBaseURL). Pass the origin — e.g. "https://scrapebadger.com" — not a full endpoint path; the client appends "/v1/web/scrape" and "/v1/account/me" itself. A trailing slash is ignored.

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient supplies the http.Client used for every request, replacing the default. Use it to set a different timeout, install a transport with proxy or TLS settings, or route calls through instrumentation.

type Request

type Request struct {
	// The URL to scrape. Must be a valid HTTP or HTTPS URL. Private IPs and cloud
	// metadata endpoints are blocked for security.
	URL string `json:"url"`

	// Scraping engine tier to use. ScrapeBadger automatically selects the best
	// approach. Default EngineAuto.
	Engine Engine `json:"engine,omitempty"`

	// Output format for the scraped content. Default FormatHTML.
	Format Format `json:"format,omitempty"`

	// Force JavaScript rendering before extracting content. Automatically switches to
	// the browser engine. Use this for single-page applications or pages that load
	// content dynamically. Default false.
	RenderJS bool `json:"render_js,omitempty"`

	// CSS selector or XPath expression to wait for before extracting content. Only
	// works with browser engines. If RenderJS is false and this is set, JS rendering
	// is forced automatically.
	WaitFor string `json:"wait_for,omitempty"`

	// Maximum time in milliseconds to wait for the WaitFor selector to appear.
	// Range: 1000 – 120000. Default 30000.
	WaitTimeout int `json:"wait_timeout,omitempty"`

	// Additional milliseconds to wait after the page has finished loading, before
	// extracting content. Useful for pages with animations or delayed rendering. Only
	// works with browser engines. Range: 0 – 30000.
	WaitAfterLoad int `json:"wait_after_load,omitempty"`

	// A list of browser actions to perform before extracting content. Forces the
	// browser engine.
	JSScenario []JSAction `json:"js_scenario,omitempty"`

	// A unique identifier to persist cookies, fingerprint, and browser storage across
	// multiple requests. Use the same SessionID to maintain login state or continue a
	// browsing session.
	SessionID string `json:"session_id,omitempty"`

	// Maximum number of retry attempts when a blocking page is detected. Retries are
	// free — you only pay for the final successful engine. Range: 0 – 10. Default 3.
	RetryCount *int `json:"retry_count,omitempty"`

	// Whether to automatically retry when a blocking page is detected. Set to false to
	// get the blocked response immediately. Default true.
	RetryOnBlock *bool `json:"retry_on_block,omitempty"`

	// ISO 3166-1 alpha-2 country code for proxy geo-targeting. Routes the request
	// through a proxy in the specified country.
	Country string `json:"country,omitempty"`

	// Additional HTTP headers to include in the request to the target URL.
	CustomHeaders map[string]string `json:"custom_headers,omitempty"`

	// Capture a full-page screenshot (PNG). Forces the browser engine. Returned as
	// base64 in Response.ScreenshotURL. Default false.
	Screenshot bool `json:"screenshot,omitempty"`

	// Record a video of the browser session (animated GIF). Forces the browser engine.
	// Returned as base64 in Response.VideoURL. Adds +3 credits. Useful for debugging,
	// visual verification, or monitoring how a page loads. Default false.
	Video bool `json:"video,omitempty"`

	// Attempt to bypass detected anti-bot protection using registered solvers. Adds +5
	// credits when a solver is invoked. Only triggered when blocking is actually
	// detected. Default false.
	AntiBot bool `json:"anti_bot,omitempty"`

	// Allow automatic escalation to more powerful engines when the initial engine is
	// blocked. Escalation path: curl_cffi → browser → windows_chrome. You only pay for
	// the engine that succeeds — costs are not cumulative. Without this flag, only the
	// selected engine is tried. Default false.
	Escalate bool `json:"escalate,omitempty"`

	// Maximum credits to spend on this request. The request fails with ErrBadRequest
	// if the estimated cost would exceed this budget. Useful for controlling costs
	// when using Escalate or AntiBot. Minimum: 1.
	MaxCost int `json:"max_cost,omitempty"`

	// Run AI-powered extraction on the scraped content using the instruction in
	// AIPrompt. Adds +2 credits. The scrape result is still returned even if AI
	// extraction fails.
	AIExtract bool `json:"ai_extract,omitempty"`

	// Natural language instruction for AI data extraction. Required when AIExtract is
	// true. Maximum 2000 characters.
	AIPrompt string `json:"ai_prompt,omitempty"`
}

Request is the body of POST /v1/web/scrape. It is a wire contract with an API this package does not control, so the json tags — not the Go field names — are what matter; renaming one silently stops sending that knob rather than failing to compile.

Only URL is required, and Client.Scrape sets it from its own argument. Everything else is omitempty so an unset field lets the API apply its own default, with one deliberate exception: RetryCount and RetryOnBlock are pointers because their defaults are *non-zero* (3 and true), so a plain int/bool could never transmit an explicit 0/false — omitempty would erase it and the API would keep retrying. The remaining flags all default to false, where "omitted" and "false" mean the same thing, so they stay plain bools.

Costs worth knowing before setting these: Video is +3 credits, AntiBot +5 (only when a solver actually fires), AIExtract +2. Escalate is free until it escalates — you pay only for the engine that ultimately succeeds, not for each attempt.

type Response

type Response struct {
	// Whether the scrape completed successfully. False when all retries are exhausted
	// and the page is still blocked.
	Success bool `json:"success"`

	// The final URL after any redirects.
	URL string `json:"url"`

	// HTTP status code from the target URL.
	StatusCode int `json:"status_code"`

	// The scraped content in the requested format. Empty when Success is false.
	Content string `json:"content"`

	// The output format used.
	Format Format `json:"format"`

	// The engine tier that produced the final result. A plain string, not Engine:
	// the tiers that can *answer* (http, curl_cffi, patchright, windows_chrome, …)
	// are a wider set than the two a request can ask for.
	EngineUsed string `json:"engine_used"`

	// Total credits charged for this request, including engine cost, solver, and AI
	// extraction.
	CreditsUsed int `json:"credits_used"`

	// Total request processing time in milliseconds.
	DurationMS int `json:"duration_ms"`

	// Number of retry attempts performed. 0 if the first attempt succeeded.
	RetriesUsed int `json:"retries_used"`

	// Size of the returned content in bytes.
	ContentLength int64 `json:"content_length"`

	// Base64-encoded PNG screenshot of the page, despite the name. Only present when
	// Request.Screenshot was set.
	ScreenshotURL string `json:"screenshot_url"`

	// Base64-encoded animated GIF of the browser session, despite the name. Only
	// present when Request.Video was set.
	VideoURL string `json:"video_url"`

	// HTTP response headers from the target URL. Typed as map[string]any rather than
	// map[string]string because a repeated header (set-cookie is the usual one) can
	// arrive as an array, and a stricter type fails the *whole* decode — losing a
	// scrape that was already paid for over a field nothing reads structurally.
	Headers map[string]any `json:"headers"`

	// Whether a blocking page was detected during scraping.
	BlockingDetected bool `json:"blocking_detected"`

	// Details about the detected blocking page. Only populated when BlockingDetected.
	BlockingDetails BlockingDetails `json:"blocking_details"`

	// Anti-bot systems detected on the page.
	AntiBotSystems []DetectedSystem `json:"antibot_systems"`

	// CAPTCHA systems detected on the page.
	CaptchaSystems []DetectedSystem `json:"captcha_systems"`

	// Whether the anti-bot solver successfully bypassed the protection.
	AntiBotSolved bool `json:"anti_bot_solved"`

	// Name of the solver that successfully bypassed the block. Empty if none was used.
	SolverUsed string `json:"solver_used"`

	// Structured data extracted by the LLM based on Request.AIPrompt. The shape
	// depends on the prompt — object, array, or bare string — so it stays raw for the
	// caller to unmarshal into its own type. Null when AIExtract was false or the
	// extraction failed.
	AIExtraction json.RawMessage `json:"ai_extraction"`

	// The LLM model used for extraction (e.g. gpt-4o-mini). Empty when AI extraction
	// was not used.
	AIModel string `json:"ai_model"`

	// Error message if AI extraction failed. The scrape result is still returned.
	AIError string `json:"ai_error"`

	// Error and Message are undocumented but real: the API sometimes answers HTTP 200
	// with success:false and the reason in one of these instead of returning a non-2xx
	// (an unreachable target, for instance). Which one it uses varies, hence
	// FailureReason. Message doubles as the human-readable note on the async-unblock
	// envelope below.
	Error   string `json:"error,omitempty"`
	Message string `json:"message,omitempty"`

	// Async-unblock envelope — also undocumented, but observed in production. When a
	// target needs unblocking, the API answers 202 with a "still working" body instead
	// of a render: Status is "running", PollURL is where to check back (often
	// root-relative, e.g. "/v1/web/unblock/<id>"), and JobID identifies the in-flight
	// unblock. All three are absent on a normal render, so IsUnblockPending is the way
	// to tell. Client.Scrape polls these to completion on the caller's behalf.
	JobID   string `json:"job_id,omitempty"`
	Status  string `json:"status,omitempty"`
	PollURL string `json:"poll_url,omitempty"`
}

Response is the API's reply to a scrape. The same shape covers three different outcomes, distinguishable only by which fields are populated: a finished render (Success with Content), a failure (Success false — either an HTTP 422 with BlockingDetails filled in, or, as observed in production, an HTTP 200 whose reason sits in Error or Message), and an async-unblock acknowledgement (see the PollURL block below).

A failure usually does not arrive as this object directly: the API commonly nests it under "data" and puts the reason above it, split between a code and a human-readable detail. That wrapper is unwrapped during decoding and its two halves folded into Error and Message, so a caller sees one shape either way.

Nullable JSON fields are typed as plain values rather than pointers: unmarshaling null into a string, struct, or slice is a no-op that leaves the zero value, so `"content": null` reads as "" and `"blocking_details": null` as a zero struct. That keeps callers free of nil checks on fields the API nulls routinely.

The enum-typed fields are named strings, so an unrecognized value decodes without error rather than failing the response — the API can add a format or engine tier at any time. Anything that branches on one must handle a value outside the constants this package declares.

func (Response) FailureReason

func (r Response) FailureReason() string

FailureReason returns the most specific reason the API gave for a non-success response, checking the fields in order of specificity: the top-level error, then message, then the nested blocking details (where a pre-flight engine rejection surfaces), then the AI extraction error. Empty when no reason was given at all.

func (Response) IsUnblockPending

func (r Response) IsUnblockPending() bool

IsUnblockPending reports whether this is the async "still unblocking the target" envelope (a 202 carrying a poll URL) rather than a finished render.

type ScrollDirection

type ScrollDirection string

ScrollDirection is the way an ActionScroll step travels.

const (
	// ScrollUp scrolls toward the top of the page.
	ScrollUp ScrollDirection = "up"

	// ScrollDown scrolls toward the bottom of the page.
	ScrollDown ScrollDirection = "down"
)

type Subscription

type Subscription struct {
	PlanCode         string `json:"plan_code"`
	PlanTitle        string `json:"plan_title"`
	BillingCadence   string `json:"billing_cadence"`
	Status           string `json:"status"`
	CurrentPeriodEnd string `json:"current_period_end"`
	MonthlyCredits   int    `json:"monthly_credits"`
}

Subscription is the recurring-plan half of an Account, nil on a pure pay-as-you-go account. MonthlyCredits is the allowance the plan grants each period; CurrentPeriodEnd is when Account.SubscriptionCreditsBalance resets.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL