billing

package
v1.0.3 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package billing validates App Store and Google Play subscription receipts and parses the two stores' server notifications, mapping both into one normalized model. It is deliberately self-contained: no database, no proto, no server wiring. Callers hand it project store credentials / receipts and get back a NormalizedSubscription or NormalizedNotification; the caller owns persistence and entitlement derivation.

The store is the source of truth; moth is a validating mirror. Apple StoreKit 2 signed transactions and App Store Server Notifications V2 are verified locally against Apple's root CA via their x5c certificate chain (NOT a JWKS — see jws.go), and authoritative renewal state comes from the App Store Server API. Google Play state comes from the Play Developer API (purchases.subscriptionsv2.get); RTDN pushes are nudges to re-read, never trusted for state. Every outbound client takes an injectable HTTP Doer, overridable base/token URLs, and an injectable clock, so the whole engine is testable against httptest doubles with no network — mirroring internal/oidc.

Index

Constants

View Source
const (
	AppleStoreKitProdURL    = "https://api.storekit.itunes.apple.com"
	AppleStoreKitSandboxURL = "https://api.storekit-sandbox.itunes.apple.com"
)

App Store Server API hosts. The API mirrors data across environments: a production transaction is unknown to the sandbox host and vice-versa, so a production lookup that 404s is retried against sandbox (Apple's documented fallback rule).

View Source
const (
	StoreApple  = "apple"
	StoreGoogle = "google"
	StoreStripe = "stripe"
)

Store identifies which store a subscription originates from.

View Source
const (
	EnvSandbox    = "sandbox"
	EnvProduction = "production"
)

Environment distinguishes sandbox/test receipts from live ones. Stored so a project never conflates a tester's sandbox subscription with production.

View Source
const (
	StatusActive         = "active"
	StatusTrialing       = "trialing"
	StatusInGracePeriod  = "in_grace_period"
	StatusInBillingRetry = "in_billing_retry" // Google "on hold"
	StatusPaused         = "paused"
	StatusExpired        = "expired"
	StatusRevoked        = "revoked"
)

Subscription status enum, minimal and mapped identically from both stores (plan/11 §Model). These string constants are the contract the server agent maps onto its subscription_status column and entitlement-derivation matrix:

active, trialing, in_grace_period, in_billing_retry -> entitlement GRANTED
                                                        (grace/retry keep
                                                        access per store
                                                        policy)
paused, expired, revoked                            -> NOT granted
View Source
const (
	GooglePlayBaseURL = "https://androidpublisher.googleapis.com"
	GoogleTokenURL    = "https://oauth2.googleapis.com/token"
)

Google Play Developer API defaults, overridable for tests.

View Source
const (
	GoogleNotifRecovered            = 1
	GoogleNotifRenewed              = 2
	GoogleNotifCanceled             = 3
	GoogleNotifPurchased            = 4
	GoogleNotifOnHold               = 5
	GoogleNotifInGracePeriod        = 6
	GoogleNotifRestarted            = 7
	GoogleNotifPriceChangeConfirmed = 8
	GoogleNotifDeferred             = 9
	GoogleNotifPaused               = 10
	GoogleNotifPauseScheduleChanged = 11
	GoogleNotifRevoked              = 12
	GoogleNotifExpired              = 13
)

Google RTDN subscriptionNotification.notificationType codes.

View Source
const StripeAPIBaseURL = "https://api.stripe.com"

StripeAPIBaseURL is the production Stripe REST API host, overridable for tests via StripeClient.BaseURL. Stripe has no separate sandbox host — test mode is selected by the secret key (sk_test_...) and reported back as livemode=false.

Variables

View Source
var (
	ErrMalformed        = errors.New("billing: malformed token")
	ErrInvalidSignature = errors.New("billing: invalid signature")
	ErrUntrustedChain   = errors.New("billing: certificate chain not trusted")
	ErrBundleMismatch   = errors.New("billing: bundle id mismatch")
	ErrNotFound         = errors.New("billing: not found")
)

Validation / verification errors. Callers map ErrMalformed and the verify failures to connect.CodeInvalidArgument, and a store 404 to connect.CodeNotFound (see ErrNotFound below).

Functions

func AppleRoots

func AppleRoots() *x509.CertPool

AppleRoots returns the CertPool moth verifies Apple JWS chains against — the embedded Apple Root CA - G3. Tests inject their own pool instead.

func AuthenticatePushToken

func AuthenticatePushToken(got, want string) bool

AuthenticatePushToken verifies a shared-secret path token guarding the RTDN webhook, in constant time. moth generates a random token per project and registers the Pub/Sub push endpoint as /billing/google/rtdn/{slug}?token=SECRET (or as a path segment); Google replays it on every push, and a request without the exact secret is dropped.

This is the simple, self-contained option. The alternative — verifying the OIDC identity token Google signs the push with (aud = the endpoint, iss = accounts.google.com, email = the push service account) — needs the Google JWKS verifier (internal/oidc) and the configured service-account email; it is preferable when the endpoint is public and the operator cannot keep a URL secret. moth ships the shared-secret path and documents the OIDC path here.

func ParseP8

func ParseP8(data []byte) (*ecdsa.PrivateKey, error)

ParseP8 re-exports oidc.ParseP8: an Apple .p8 In-App-Purchase key is the same PEM-wrapped PKCS#8 EC P-256 as a Sign-in-with-Apple key.

func StripeMicrosForUnitAmount

func StripeMicrosForUnitAmount(unitAmount int64, currency string) int64

StripeMicrosForUnitAmount converts Stripe's unit_amount back to moth micros for the currency.

func StripeRecurringForPeriod

func StripeRecurringForPeriod(period string) (interval string, count int, err error)

StripeRecurringForPeriod maps a moth product billing_period onto Stripe's recurring[interval]/[interval_count]. It accepts the same free-form vocabulary as the Apple/Google catalog sync (setup.parseBillingPeriod): words ("weekly", "monthly", "two_month", "quarterly", "half_year", "yearly" and their synonyms) and the ISO-8601 forms P1W/P1M/P2M/P3M/P6M/P1Y.

func StripeTrialDays

func StripeTrialDays(trial string) (int64, error)

StripeTrialDays maps a moth product trial_period onto Stripe's subscription_data[trial_period_days]. It accepts the same vocabulary as StripeRecurringForPeriod plus arbitrary ISO-8601 durations of a single unit (P3D, P2W, ...); calendar units use the usual 30-day month / 365-day year approximation since Stripe trials are day-denominated. Empty means no trial (0, nil).

func StripeUnitAmountForMicros

func StripeUnitAmountForMicros(micros int64, currency string) (int64, error)

StripeUnitAmountForMicros converts a moth price (micros of whole currency units) to Stripe's unit_amount for the currency. Amounts that are not representable in the currency's minor unit are rejected rather than silently rounded.

func VerifyStripeSignature

func VerifyStripeSignature(payload []byte, header, secret string, now time.Time, tolerance time.Duration) error

VerifyStripeSignature verifies a Stripe-Signature header (t=<unix>,v1=<hex>[,v1=<hex>...]) against the endpoint's signing secret: HMAC-SHA256 over "<t>.<payload>", constant-time compare against every v1 (Stripe sends several during secret rolls), and a ±tolerance window on t (skipped when tolerance <= 0). A malformed header wraps ErrMalformed; every other failure — empty secret included — wraps ErrInvalidSignature.

Types

type AppleClient

type AppleClient struct {
	// BaseURL is the primary host (defaults to production).
	BaseURL string
	// SandboxURL is the fallback host tried on a production 404 (defaults to
	// the sandbox host; set "" to disable fallback, e.g. in tests).
	SandboxURL string
	IssuerID   string // ASC issuer id (iss)
	KeyID      string // In-App-Purchase key id (JWS kid)
	BundleID   string // bid claim + verifier bundle check
	Key        *ecdsa.PrivateKey
	HTTPC      Doer
	Now        func() time.Time
	// Verifier verifies the signed transaction/renewal JWS in responses.
	// Defaults to a verifier over Apple's real roots bound to BundleID.
	Verifier *AppleVerifier
}

AppleClient calls the App Store Server API, authenticating each request with a short-lived ES256 JWT minted from the project's In-App-Purchase .p8 key (same mechanism as App Store Connect, distinct key type and audience). Every signed blob it receives is verified through Verifier before use.

func (*AppleClient) GetAllSubscriptionStatuses

func (c *AppleClient) GetAllSubscriptionStatuses(ctx context.Context, originalTransactionID string) (NormalizedSubscription, error)

GetAllSubscriptionStatuses resolves an originalTransactionId to authoritative state: it fetches the subscription-group statuses, verifies the signed transaction + renewal info of the matching last transaction, and returns a NormalizedSubscription. On a production 404 it retries sandbox (the documented fallback), so a sandbox transaction resolves without the caller guessing the environment.

func (*AppleClient) GetTransactionInfo

func (c *AppleClient) GetTransactionInfo(ctx context.Context, transactionID string) (*JWSTransaction, error)

GetTransactionInfo fetches and verifies a single transaction by transactionId, returning its decoded payload. Production 404 falls back to sandbox.

func (*AppleClient) Token

func (c *AppleClient) Token() (string, error)

Token mints the ES256 request JWT: header alg/kid/typ; claims iss/iat/exp/aud/bid (Apple's "Generating JSON Web Tokens for API requests").

type AppleVerifier

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

AppleVerifier verifies Apple JWS blobs (transactions, renewal info, notifications) against a trust anchor. Zero value is not usable; use NewAppleVerifier.

func NewAppleVerifier

func NewAppleVerifier(roots *x509.CertPool, expectBundle string, now func() time.Time) *AppleVerifier

NewAppleVerifier returns a verifier over roots (defaults to AppleRoots when nil — tests pass a test CA pool). now defaults to time.Now. expectBundle, when non-empty, is required to match every verified transaction's bundleId, rejecting a receipt minted for another app.

func (*AppleVerifier) VerifyNotification

func (v *AppleVerifier) VerifyNotification(signedPayload string) (*NormalizedNotification, error)

VerifyNotification verifies an App Store Server Notification V2 signedPayload (same x5c chain as a transaction), then verifies the nested transaction / renewal-info JWS it carries, and returns a NormalizedNotification. The notification's Subscription is best-effort: the caller must still re-read authoritative state via the App Store Server API before granting.

func (*AppleVerifier) VerifyRenewalInfo

func (v *AppleVerifier) VerifyRenewalInfo(jws string) (*JWSRenewalInfo, error)

VerifyRenewalInfo verifies a signed renewal-info JWS and returns its decoded payload.

func (*AppleVerifier) VerifyTransaction

func (v *AppleVerifier) VerifyTransaction(jws string) (*JWSTransaction, error)

VerifyTransaction verifies a signed transaction JWS and returns its decoded payload. It rejects a tampered signature, an untrusted or expired chain, and (when expectBundle is set) a mismatched bundle id.

type DeveloperNotification

type DeveloperNotification struct {
	Version                  string `json:"version"`
	PackageName              string `json:"packageName"`
	EventTimeMillis          string `json:"eventTimeMillis"`
	SubscriptionNotification *struct {
		Version          string `json:"version"`
		NotificationType int    `json:"notificationType"`
		PurchaseToken    string `json:"purchaseToken"`
		SubscriptionID   string `json:"subscriptionId"`
	} `json:"subscriptionNotification"`
	TestNotification *struct {
		Version string `json:"version"`
	} `json:"testNotification"`
}

DeveloperNotification is the Play RTDN payload carried inside a Pub/Sub push.

func ParsePubSubPush

func ParsePubSubPush(body []byte) (notif *DeveloperNotification, messageID string, err error)

ParsePubSubPush parses a Pub/Sub push envelope and decodes the embedded DeveloperNotification. The RTDN is a nudge: the caller re-reads state via GetSubscriptionV2 using the returned purchaseToken. MessageID is the Pub/Sub message id, usable as the idempotency key.

type Doer

type Doer interface {
	Do(req *http.Request) (*http.Response, error)
}

Doer is the subset of *http.Client the package needs; injectable so tests and the server package can point it at doubles. Mirrors oidc.Doer.

type GoogleClient

type GoogleClient struct {
	// BaseURL defaults to GooglePlayBaseURL; tests point it at a double.
	BaseURL     string
	PackageName string
	Tokens      *GoogleTokenSource
	HTTPC       Doer
}

GoogleClient calls the Google Play Developer API for one project's package.

func (*GoogleClient) AcknowledgeSubscription

func (c *GoogleClient) AcknowledgeSubscription(ctx context.Context, subscriptionID, purchaseToken string) error

AcknowledgeSubscription acknowledges a purchase (purchases.subscriptions. acknowledge). Google auto-refunds a subscription that is not acknowledged within three days, so the caller acknowledges once acknowledgementState is "acknowledgementStatePending".

func (*GoogleClient) GetSubscriptionV2

func (c *GoogleClient) GetSubscriptionV2(ctx context.Context, purchaseToken string) (NormalizedSubscription, SubscriptionPurchaseV2, error)

GetSubscriptionV2 resolves a purchaseToken to authoritative subscription state and returns a NormalizedSubscription. A 404 (token for another package/project, or an unknown token) surfaces as ErrNotFound.

type GoogleServiceAccount

type GoogleServiceAccount struct {
	ClientEmail   string `json:"client_email"`
	PrivateKeyID  string `json:"private_key_id"`
	PrivateKeyPEM string `json:"private_key"`
	TokenURI      string `json:"token_uri"`
	// contains filtered or unexported fields
}

GoogleServiceAccount is the subset of a Google service-account JSON key moth needs to mint an androidpublisher access token.

func ParseServiceAccount

func ParseServiceAccount(data []byte) (*GoogleServiceAccount, error)

ParseServiceAccount parses a service-account JSON key and its PEM private key. The parsed key is stored encrypted under the master key by the caller; this only turns bytes into a usable signer.

type GoogleTokenSource

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

GoogleTokenSource exchanges a service-account assertion for an androidpublisher OAuth2 access token (RFC 7523 JWT-bearer grant) and caches it until shortly before expiry. Safe for concurrent use.

func NewGoogleTokenSource

func NewGoogleTokenSource(sa *GoogleServiceAccount, tokenURL string, httpc Doer, now func() time.Time) *GoogleTokenSource

NewGoogleTokenSource returns a cached token source. tokenURL defaults to the service account's token_uri (or GoogleTokenURL); httpc and now default to a timeout-bounded client and time.Now.

func (*GoogleTokenSource) Token

func (ts *GoogleTokenSource) Token(ctx context.Context) (string, error)

Token returns a valid access token, minting a fresh one when the cache is within a minute of expiry.

type JWSRenewalInfo

type JWSRenewalInfo struct {
	OriginalTransactionID  string `json:"originalTransactionId"`
	ProductID              string `json:"productId"`
	AutoRenewProductID     string `json:"autoRenewProductId"`
	AutoRenewStatus        int    `json:"autoRenewStatus"` // 1 on, 0 off
	ExpirationIntent       int    `json:"expirationIntent"`
	GracePeriodExpiresDate int64  `json:"gracePeriodExpiresDate"`
	Environment            string `json:"environment"`
}

JWSRenewalInfo is the JWSRenewalInfoDecodedPayload subset moth reads from a signed renewal-info blob.

type JWSTransaction

type JWSTransaction struct {
	TransactionID               string `json:"transactionId"`
	OriginalTransactionID       string `json:"originalTransactionId"`
	BundleID                    string `json:"bundleId"`
	ProductID                   string `json:"productId"`
	SubscriptionGroupIdentifier string `json:"subscriptionGroupIdentifier"`
	PurchaseDate                int64  `json:"purchaseDate"`
	ExpiresDate                 int64  `json:"expiresDate"`
	// Type is e.g. "Auto-Renewable Subscription".
	Type string `json:"type"`
	// Environment is "Sandbox" or "Production".
	Environment string `json:"environment"`
	// OfferType is non-zero for introductory / promotional / trial offers;
	// used to distinguish trialing from active.
	OfferType int `json:"offerType"`
	// RevocationDate is set when Apple revoked the purchase (refund, family
	// removal). Non-zero => revoked.
	RevocationDate int64 `json:"revocationDate"`
	// Price is the store-reported price in milliunits (thousandths) of Currency,
	// as the buyer's storefront charged it. Currency is the ISO-4217 code. Both
	// are present on StoreKit 2 transactions since 2023; zero/empty on older
	// receipts, in which case the caller falls back to the catalog price.
	Price    int64  `json:"price"`
	Currency string `json:"currency"`
}

JWSTransaction is the JWSTransactionDecodedPayload subset moth reads from a StoreKit 2 signed transaction. Dates are Apple's milliseconds-since-epoch.

type NormalizedNotification

type NormalizedNotification struct {
	Store          string
	Type           string
	Subtype        string
	NotificationID string
	Subscription   NormalizedSubscription
	Raw            json.RawMessage
}

NormalizedNotification is the store-agnostic shape of a store notification. Type/Subtype carry the store's own notification vocabulary (for the audit trail); NotificationID is the store's unique id used to dedupe replays. The embedded Subscription is best-effort from the notification body — the caller MUST re-read authoritative state from the store API before trusting it for entitlement changes (plan/11: "the notification is a nudge, not a payload to trust").

type NormalizedSubscription

type NormalizedSubscription struct {
	// Store is StoreApple or StoreGoogle.
	Store string
	// ProductID is the store product identifier (Apple productId / Google
	// line-item productId). Maps to a moth product tier downstream.
	ProductID string
	// StoreTransactionID is the stable store identity moth keys the
	// subscription on: originalTransactionId (Apple) or purchaseToken
	// (Google).
	StoreTransactionID string
	// SubscriptionID is the Google base-plan/subscription id, or the Apple
	// subscriptionGroupIdentifier. Empty when the store does not supply one.
	SubscriptionID string
	// Status is one of the status constants above.
	Status string
	// CurrentPeriodEnd is when the paid period ends (renewal or expiry).
	// Zero when the store did not supply an expiry.
	CurrentPeriodEnd time.Time
	// AutoRenew reflects the store's renewal flag at read time.
	AutoRenew bool
	// Environment is EnvSandbox or EnvProduction.
	Environment string
	// PriceAmountMicros is the store-reported transaction price in micros
	// (millionths) of Currency, when the store supplies it (Apple StoreKit 2
	// transactions carry price + currency; Google's subscriptionsv2 does not).
	// Zero when the store did not report a price — the caller then falls back to
	// the moth catalog price. This is the storefront-localized amount the buyer
	// actually paid, so per-currency revenue reflects real charges rather than a
	// single catalog list price.
	PriceAmountMicros int64
	// Currency is the ISO-4217 code of PriceAmountMicros, "" when the store
	// reported none.
	Currency string
	// RawState is the verified store JSON (decoded transaction / purchase),
	// persisted verbatim for audit and reconciliation.
	RawState json.RawMessage
}

NormalizedSubscription is the store-agnostic subscription state both stores map into. The server agent persists it to the subscriptions table and derives entitlements from Status. It is intentionally flat and free of any store SDK type.

type StripeAPIError

type StripeAPIError struct {
	Status  int
	Code    string
	Type    string
	Message string
}

StripeAPIError is a non-2xx (non-404) Stripe API response. Callers use errors.As to branch on the stable Code (e.g. "resource_missing" when a stored customer or price id no longer exists in the account/mode) instead of string-matching the message.

func (*StripeAPIError) Error

func (e *StripeAPIError) Error() string

type StripeCheckoutParams

type StripeCheckoutParams struct {
	// PriceID is the recurring price the session subscribes to.
	PriceID string
	// CustomerID binds the session to an existing Stripe customer.
	CustomerID string
	SuccessURL string
	CancelURL  string
	// ClientReferenceID carries the moth user id back on
	// checkout.session.completed.
	ClientReferenceID string
	// Metadata is attached to BOTH the session and (via subscription_data)
	// the resulting subscription, so the subscription itself carries the moth
	// project/user ids.
	Metadata map[string]string
	// TrialPeriodDays > 0 starts the subscription with a free trial.
	TrialPeriodDays int64
}

StripeCheckoutParams shapes a subscription-mode Checkout Session.

type StripeCheckoutSession

type StripeCheckoutSession struct {
	ID                string            `json:"id"`
	URL               string            `json:"url"`
	Subscription      string            `json:"subscription"`
	Customer          string            `json:"customer"`
	ClientReferenceID string            `json:"client_reference_id"`
	Metadata          map[string]string `json:"metadata"`
}

StripeCheckoutSession is the subset of a Checkout Session moth reads: on create the hosted URL to redirect to; on checkout.session.completed the created subscription/customer and the moth identity echoes.

type StripeClient

type StripeClient struct {
	// BaseURL defaults to StripeAPIBaseURL; tests point it at a double.
	BaseURL string
	// SecretKey is the project's sk_/rk_ secret key, sent as a Bearer token.
	SecretKey string
	HTTPC     Doer
	Now       func() time.Time
}

StripeClient calls the Stripe REST API directly (no Stripe SDK) with a project's restricted/secret key. Like the Apple and Google clients it is a struct literal with zero values meaning production defaults, so the whole engine stays testable against httptest doubles.

func (*StripeClient) CreateBillingPortalSession

func (c *StripeClient) CreateBillingPortalSession(ctx context.Context, customerID, returnURL string) (StripePortalSession, error)

CreateBillingPortalSession creates a Billing Portal session (POST /v1/billing_portal/sessions) for a customer.

func (*StripeClient) CreateCheckoutSession

func (c *StripeClient) CreateCheckoutSession(ctx context.Context, params StripeCheckoutParams) (StripeCheckoutSession, error)

CreateCheckoutSession creates a subscription-mode hosted Checkout Session (POST /v1/checkout/sessions). Checkout stays Stripe-hosted: moth never renders a card field.

func (*StripeClient) CreateCustomer

func (c *StripeClient) CreateCustomer(ctx context.Context, email string, metadata map[string]string) (StripeCustomer, error)

CreateCustomer creates a Stripe customer (POST /v1/customers). moth creates at most one per (project, user), lazily on first checkout, carrying the moth ids in metadata.

func (*StripeClient) CreatePrice

func (c *StripeClient) CreatePrice(ctx context.Context, params StripePriceParams) (StripePrice, error)

CreatePrice creates a recurring price (POST /v1/prices). Stripe prices are immutable: a price change creates a new price and re-points the tier.

func (*StripeClient) CreateProduct

func (c *StripeClient) CreateProduct(ctx context.Context, name string, metadata map[string]string) (StripeProduct, error)

CreateProduct creates a Stripe product (POST /v1/products) during catalog provisioning, carrying the moth tier identity in metadata.

func (*StripeClient) CreateWebhookEndpoint

func (c *StripeClient) CreateWebhookEndpoint(ctx context.Context, endpointURL string, events []string) (StripeWebhookEndpoint, error)

CreateWebhookEndpoint creates a webhook endpoint (POST /v1/webhook_endpoints) subscribed to events. The response carries the signing Secret exactly once — the caller stores it encrypted.

func (*StripeClient) GetPrice

func (c *StripeClient) GetPrice(ctx context.Context, id string) (StripePrice, error)

GetPrice fetches a price by id (GET /v1/prices/{id}) so provisioning can detect drift between the moth catalog and the live Stripe price. An unknown id surfaces as ErrNotFound.

func (*StripeClient) GetProduct

func (c *StripeClient) GetProduct(ctx context.Context, id string) (StripeProduct, error)

GetProduct fetches a product by id (GET /v1/products/{id}) so provisioning can detect display-name drift between the moth catalog and the live Stripe product. An unknown id surfaces as ErrNotFound.

func (*StripeClient) GetSubscription

GetSubscription resolves a Stripe subscription id (sub_...) to authoritative state (GET /v1/subscriptions/{id}) and returns it normalized alongside the partial raw decode. Webhooks are nudges: every event triggers this re-read. An unknown id surfaces as ErrNotFound.

func (*StripeClient) ListWebhookEndpoints

func (c *StripeClient) ListWebhookEndpoints(ctx context.Context) ([]StripeWebhookEndpoint, error)

ListWebhookEndpoints lists ALL of the account's webhook endpoints (GET /v1/webhook_endpoints) so setup can detect an already-provisioned endpoint. Stripe pages the list (default 10) — a moth instance hosting a portfolio of projects easily exceeds that, and a missed endpoint would make setup create duplicates and rotate the stored signing secret — so this follows has_more/starting_after to exhaustion.

func (*StripeClient) UpdateProduct

func (c *StripeClient) UpdateProduct(ctx context.Context, id, name string) (StripeProduct, error)

UpdateProduct renames a product in place (POST /v1/products/{id}). Unlike prices, Stripe product names are mutable, so a moth display-name change is a real update rather than a recreate.

func (*StripeClient) UpdateWebhookEndpoint

func (c *StripeClient) UpdateWebhookEndpoint(ctx context.Context, id string, events []string) (StripeWebhookEndpoint, error)

UpdateWebhookEndpoint re-points an existing endpoint's event subscription and re-enables it if Stripe disabled it (POST /v1/webhook_endpoints/{id}). The signing secret is NOT returned on update — only create reveals it.

type StripeCustomer

type StripeCustomer struct {
	ID    string `json:"id"`
	Email string `json:"email"`
}

StripeCustomer is the subset of a Stripe customer moth reads.

type StripeEvent

type StripeEvent struct {
	ID       string `json:"id"`
	Type     string `json:"type"`
	LiveMode bool   `json:"livemode"`
	Data     struct {
		Object json.RawMessage `json:"object"`
	} `json:"data"`
}

StripeEvent is the webhook event envelope. Data.Object is the raw embedded object, decoded on demand via CheckoutSession/SubscriptionObject — and only ever treated as a nudge: state is re-read from the API before applying.

func ParseStripeEvent

func ParseStripeEvent(payload []byte) (StripeEvent, error)

ParseStripeEvent parses a verified webhook body into the event envelope. The event id (evt_...) is the dedupe key. Malformed JSON or a missing id/type wraps ErrMalformed.

func (StripeEvent) CheckoutSession

func (e StripeEvent) CheckoutSession() (StripeCheckoutSession, error)

CheckoutSession decodes Data.Object as a Checkout Session (checkout.session.completed events).

func (StripeEvent) SubscriptionObject

func (e StripeEvent) SubscriptionObject() (StripeSubscription, error)

SubscriptionObject decodes Data.Object as a subscription (customer.subscription.* events), keeping the verbatim object as Raw.

type StripePortalSession

type StripePortalSession struct {
	ID  string `json:"id"`
	URL string `json:"url"`
}

StripePortalSession is a Billing Portal session: the URL is where the user manages payment methods, invoices and cancellation, Stripe-hosted.

type StripePrice

type StripePrice struct {
	ID            string
	ProductID     string
	UnitAmount    int64
	Currency      string
	Interval      string
	IntervalCount int
	Active        bool
}

StripePrice is the subset of a Stripe price moth reads, flattened. Currency is upper-cased ISO-4217 to compare directly against the moth catalog.

type StripePriceParams

type StripePriceParams struct {
	ProductID string
	// Currency is ISO-4217 in any case; Stripe is sent the lowercase form.
	Currency string
	// UnitAmount is in the currency's minor units (cents).
	UnitAmount int64
	// Interval is day|week|month|year (see StripeRecurringForPeriod).
	Interval      string
	IntervalCount int
	Metadata      map[string]string
}

StripePriceParams shapes a recurring price for a provisioned product.

type StripeProduct

type StripeProduct struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

StripeProduct is the subset of a Stripe product moth reads.

type StripeSubscription

type StripeSubscription struct {
	ID                string `json:"id"`
	Status            string `json:"status"`
	CancelAtPeriodEnd bool   `json:"cancel_at_period_end"`
	// CurrentPeriodEnd (unix seconds) lives on the subscription in older
	// Stripe API versions; current versions expose it per item instead —
	// normalization reads both and prefers whichever is set.
	CurrentPeriodEnd int64  `json:"current_period_end"`
	Customer         string `json:"customer"`
	LiveMode         bool   `json:"livemode"`
	TrialEnd         int64  `json:"trial_end"`
	// PauseCollection is non-nil while payment collection is paused.
	PauseCollection *struct {
		Behavior string `json:"behavior"`
	} `json:"pause_collection"`
	Metadata map[string]string `json:"metadata"`
	Items    struct {
		Data []StripeSubscriptionItem `json:"data"`
	} `json:"items"`

	// Raw is the verbatim JSON this struct was decoded from (the API response
	// body or a webhook data.object), carried into RawState for audit.
	Raw json.RawMessage `json:"-"`
}

StripeSubscription is the subset of a Stripe subscription moth reads.

type StripeSubscriptionItem

type StripeSubscriptionItem struct {
	// CurrentPeriodEnd (unix seconds) is where current Stripe API versions
	// report the period end.
	CurrentPeriodEnd int64 `json:"current_period_end"`
	Price            struct {
		ID         string `json:"id"`
		Product    string `json:"product"`
		UnitAmount int64  `json:"unit_amount"`
		Currency   string `json:"currency"`
		Recurring  struct {
			Interval      string `json:"interval"`
			IntervalCount int    `json:"interval_count"`
		} `json:"recurring"`
	} `json:"price"`
}

StripeSubscriptionItem is the subset of a subscription item moth reads.

type StripeWebhookEndpoint

type StripeWebhookEndpoint struct {
	ID            string   `json:"id"`
	URL           string   `json:"url"`
	Status        string   `json:"status"`
	EnabledEvents []string `json:"enabled_events"`
	Secret        string   `json:"secret"`
}

StripeWebhookEndpoint is the subset of a Stripe webhook endpoint moth reads. Secret (whsec_...) is returned by Stripe only on create.

type SubscriptionPurchaseV2

type SubscriptionPurchaseV2 struct {
	SubscriptionState string `json:"subscriptionState"`
	LineItems         []struct {
		ProductID        string `json:"productId"`
		ExpiryTime       string `json:"expiryTime"` // RFC3339
		AutoRenewingPlan *struct {
			AutoRenewEnabled bool `json:"autoRenewEnabled"`
		} `json:"autoRenewingPlan"`
		// OfferDetails is present while the line item is on a base-plan offer
		// (a free trial or an introductory/promotional price). A non-empty
		// OfferID means the current period is discounted/free; once it renews
		// onto the plain base plan the field is absent. moth uses this to
		// classify the trialing period, mirroring Apple's offerType.
		OfferDetails *struct {
			OfferID    string `json:"offerId"`
			BasePlanID string `json:"basePlanId"`
		} `json:"offerDetails"`
	} `json:"lineItems"`
	// TestPurchase is present only for license-tester / sandbox purchases.
	TestPurchase         *struct{} `json:"testPurchase"`
	AcknowledgementState string    `json:"acknowledgementState"`
}

SubscriptionPurchaseV2 is the subset of the purchases.subscriptionsv2.get response moth reads.

Jump to

Keyboard shortcuts

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