Help us shape the future of Go! Take the 2026 Go Developer Survey

stripe

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 28, 2026 License: MIT Imports: 16 Imported by: 1

Documentation

Overview

Package stripe is payforge's Stripe adapter. Client implements payforge.Billing against the Stripe API using the official stripe-go library.

Everything written to Stripe carries the spec's app in metadata (payforge.MetaApp), so several products can share one account, and every creating call carries an idempotency key. A secret key is passed to New and is never logged or returned in an error.

Index

Constants

This section is empty.

Variables

View Source
var ErrArchived = errors.New("stripe: product is archived")

ErrArchived means a product the spec needs is archived at the account. Sync does not bring archived objects back; unarchive it or choose another plan ID.

View Source
var ErrDeleted = errors.New("stripe: product was deleted")

ErrDeleted means a product was deleted at Stripe. A deleted product's ID cannot be used again, so the plan needs a new ID in the spec.

View Source
var ErrForeign = errors.New("stripe: object belongs to another app")

ErrForeign means an object at the account holds a name payforge uses but belongs to another product, so sync leaves it alone and stops.

Functions

func ProductID

func ProductID(key string) string

ProductID is the ID of the Stripe product for a product key from payforge.ProviderPlanFor. Stripe lets the creator choose a product's ID, and a product cannot be looked up by key, so the ID is derived from the key.

Types

type Client

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

Client implements payforge.Billing for one spec on one Stripe account.

func New

func New(spec *pricingspec.Spec, cfg Config) (*Client, error)

New returns a Client. It fails with payforge.ErrNotConfigured when there is no secret key, so a host that reaches this point without one is in free mode and should use payforge.Disabled, and with the spec's errors when the spec is invalid.

func (*Client) Account

func (c *Client) Account(ctx context.Context, accountID string) (payforge.AccountState, error)

Account reads a connected account's state.

func (*Client) CheckoutPlan

func (c *Client) CheckoutPlan(ctx context.Context, in payforge.PlanCheckout) (payforge.Session, error)

CheckoutPlan starts a subscription checkout for a self-serve plan.

The price is found by lookup key, so the catalog must have been synced to the account. A subject that already holds a granting subscription is refused with payforge.ErrAlreadySubscribed. Any plan checkout of this app still open for the subject is expired first, so only the newest can be paid and two tabs cannot both subscribe. Two checkouts made at the same moment can still both miss each other unless Config.Locker is set, which serializes them per subject. A plan with a trial starts one without asking for a card, and the subscription is canceled at the end if the buyer added none.

func (*Client) CheckoutProduct

func (c *Client) CheckoutProduct(ctx context.Context, in payforge.ProductCheckout) (payforge.Session, error)

CheckoutProduct starts a one-time purchase checkout.

A fixed-price product is sold at its synced price. A seller-set product is sold at the amount given, which must reach the product's minimum. A Reference makes a retry return the same session.

func (*Client) CheckoutSale

func (c *Client) CheckoutSale(ctx context.Context, in payforge.Sale) (payforge.Session, error)

CheckoutSale starts a checkout whose proceeds go to the seller.

It is a destination charge made on the seller's behalf: the seller is the merchant on the buyer's statement, the money is transferred to the seller's account, and the platform's fee is taken as the application fee. The fee is the seller plan's rate when it sets one, else the marketplace's, rounded down to a cent. A seller whose account cannot take charges is refused with payforge.ErrSellerNotReady before any checkout is made, and an account this app did not create with payforge.ErrForeignAccount. The fee follows Sale.SellerPlan, so take it from your own record of the seller, never from a request. For a fixed-price product the fee is computed from the spec's amount: keep the catalog synced, or Stripe refuses a fee larger than a lower price.

func (*Client) CreateAccount

func (c *Client) CreateAccount(ctx context.Context, seller payforge.Subject, country string) (string, error)

CreateAccount returns the Express account for a seller, creating it once.

An idempotency key from the app and the seller makes a second call return the account the first created. Stripe has no search for accounts, so a host that loses the ID cannot look it up again and should store it. The country is the seller's two-letter code and cannot be changed later.

func (*Client) Enabled

func (*Client) Enabled() bool

Enabled reports true: a Client has a key.

func (*Client) EnsureCustomer

func (c *Client) EnsureCustomer(ctx context.Context, s payforge.Subject) (string, error)

EnsureCustomer returns the Stripe customer for a subject, creating it once.

It searches by the subject's metadata first, then creates with an idempotency key. Stripe's search can lag a creation by up to a minute, so the key covers that gap: a second call in the meantime gets the customer the first created. The key covers the name and email too, because Stripe refuses a key reused with different parameters; a changed email inside that gap can therefore create a second customer instead of failing. A host that stores the customer ID need not call this again.

func (*Client) Mode

func (c *Client) Mode() Mode

Mode is the mode of the client's key. Give it to the Webhook, so events of the other mode are not acted on.

func (c *Client) OnboardingLink(ctx context.Context, accountID, refreshURL, returnURL string) (string, error)

OnboardingLink returns a one-use page where the seller gives Stripe the details it needs. The links expire in minutes; refreshURL is where Stripe sends the seller when one has, and it should call this again.

func (*Client) Plan

func (c *Client) Plan(stored string) pricingspec.Plan

Plan resolves a stored plan; an unknown plan is the spec's default plan.

func (*Client) Portal

func (c *Client) Portal(ctx context.Context, customerID, returnURL string) (string, error)

Portal opens the customer portal for a customer.

func (*Client) Subscriptions

func (c *Client) Subscriptions(ctx context.Context, customerID string) ([]payforge.SubscriptionState, error)

Subscriptions returns a customer's subscriptions that have not ended. It is what the lifecycle reads to decide an account's plan from Stripe's state, not from the order webhooks arrived in.

func (*Client) Sync

func (c *Client) Sync(ctx context.Context, dryRun bool) (SyncReport, error)

Sync makes the Stripe account hold what the spec describes: a product per priced plan and per fixed one-time product, and a price per interval found by its lookup key.

Sync is idempotent. It never deletes and never edits an amount: a price is immutable at Stripe, so a changed amount creates a new price, moves the lookup key to it and leaves the old price for existing subscribers. With dryRun it reads the account and reports what it would do without writing.

If a step fails, the report holds the steps taken so far along with the error.

type Config

type Config struct {
	// SecretKey is the Stripe secret key. It is required.
	SecretKey string
	// HTTPClient sends the requests. The default is the library's client.
	HTTPClient *http.Client
	// APIURL replaces the Stripe API's base URL. It exists for tests that stand
	// in for Stripe; leave it empty otherwise.
	APIURL string
	// AllowedURLHosts are the hosts the success, cancel, refresh and return URLs
	// may name, so a URL taken from a request cannot send a buyer from Stripe to a
	// site of an attacker's choosing. Set it to your own hostnames. It is required
	// for checkout, the portal and onboarding links. A client that only syncs the
	// catalog may leave it empty. A URL must be https and may not carry a user
	// name or password.
	//
	// With a test-mode key, localhost, 127.0.0.1 and ::1 are also accepted, over
	// http too, even when the list does not name them, for local development.
	// With a live key they are never accepted: New refuses a list that names
	// one, and a URL on one is refused, since a live checkout that returns the
	// buyer to their own machine is always a misconfiguration.
	AllowedURLHosts []string
	// Locker, when set, is held per subject around a plan checkout, under the
	// same key the lifecycle uses, so two checkouts for one subject made at once
	// by different processes run in turn. See CheckoutPlan.
	Locker payforge.Locker
	// MaxNetworkRetries is how often the library retries a request that failed
	// for a transient reason. The library's retries reuse the idempotency key.
	// Zero uses the library's default of 2.
	MaxNetworkRetries int64
}

Config configures a Client. It holds a secret: do not log it.

type Mode

type Mode string

Mode is whether a key, and the events for it, are Stripe's test mode or live mode. Objects and events of one mode are never visible to the other.

const (
	// ModeTest is Stripe's test mode: keys that start sk_test_ or rk_test_.
	ModeTest Mode = "test"
	// ModeLive is Stripe's live mode: real money.
	ModeLive Mode = "live"
)

func KeyMode

func KeyMode(secretKey string) Mode

KeyMode returns the mode of a secret or restricted key. Only a key that starts sk_test_ or rk_test_ is test mode; any other key is treated as live, so an unrecognized key is never mistaken for one that cannot move money.

type SyncAction

type SyncAction struct {
	Kind SyncKind
	// Key is the product key or the price's lookup key.
	Key    string
	Detail string
}

SyncAction is one step of a sync.

type SyncKind

type SyncKind string

SyncKind is what sync did, or would do, to one object.

const (
	SyncCreateProduct SyncKind = "create_product"
	SyncUpdateProduct SyncKind = "update_product"
	SyncCreatePrice   SyncKind = "create_price"
	// SyncReplacePrice creates a new price because a price is immutable and the
	// spec's amount, currency or interval changed, and moves the lookup key to
	// it. The old price stays for the subscribers already on it.
	SyncReplacePrice SyncKind = "replace_price"
	SyncUnchanged    SyncKind = "unchanged"
)

type SyncReport

type SyncReport struct {
	DryRun  bool
	Actions []SyncAction
}

SyncReport lists what a sync did, in a stable order.

func (SyncReport) Changed

func (r SyncReport) Changed() bool

Changed reports whether the sync wrote, or in a dry run would write, anything.

type Webhook

type Webhook struct {
	// Mode is the mode of the API key the host uses, usually Client.Mode() or
	// KeyMode(key). It is required. An event whose livemode does not match is
	// returned as payforge.EventIgnored, to be acknowledged and not acted on, so
	// a test-mode secret configured by mistake cannot change live accounts.
	Mode Mode
	// Secrets are the signing secrets of the endpoints that deliver here. A
	// payload is accepted if any one verifies it, so the platform's endpoint and
	// the connected-account endpoint can share a handler.
	Secrets []string
	// LegacyPrefixes name host-specific metadata prefixes written before
	// payforge, such as "myapp" for myapp_organization_id and myapp_plan. They
	// are read as aliases for the subject and plan, so objects that already exist
	// at Stripe keep working.
	LegacyPrefixes []string
	// Tolerance is how old a signature may be. Zero is five minutes.
	Tolerance time.Duration
	// MaxBytes limits the body Read accepts. Zero is 1 MiB.
	MaxBytes int64
}

Webhook verifies and reads Stripe webhook requests. It holds secrets: do not log it.

func (Webhook) Parse

func (w Webhook) Parse(payload []byte, header string) (payforge.Event, error)

Parse verifies a payload against its Stripe-Signature header and reduces it to an Event.

An event of the other mode than Mode comes back as EventIgnored with only its ID, type, creation time, livemode and account, never acted on.

The signature is checked before anything else is read. Only a few stable fields are taken from the payload, so an endpoint pinned to a different API version than this library's still works. An event type the lifecycle does not use comes back as EventIgnored with only its ID and type.

func (Webhook) Read

func (w Webhook) Read(r *http.Request) (payforge.Event, error)

Read verifies the request and reduces it to an Event. It reads at most MaxBytes of the body and answers payforge.ErrPayloadTooLarge beyond that.

Jump to

Keyboard shortcuts

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