# Connect payment providers

> Turn on payments with Stripe, Braintree, the App Store or Google Play, and how purchases are attributed to your users.

An **app** connects one provider account or store listing. Create it with `apps create`; the response carries `webhook_url` and `webhook_instructions` for the one step that may need doing by hand. Credentials are encrypted at rest.

| Provider | `--provider` | Needs | Attribution to a user |
| --- | --- | --- | --- |
| Stripe | `stripe` | `secret_key` (`sk_…` or restricted `rk_…`; `webhook_secret` is filled in when the endpoint is registered automatically) | `checkout create`, or `metadata.app_user_id` / `client_reference_id` |
| Braintree | `braintree` | `merchant_id`, `public_key`, `private_key`, `environment` | `receipts submit` with the subscription id, or `customer_id_is_app_user_id` |
| App Store | `app_store` | `bundle_id` (+ `app_apple_id` for production) | `receipts submit`, or `appAccountToken` |
| Google Play | `play_store` | `service_account_json`, `package_name` | `receipts submit`, or `obfuscatedAccountId` |

## Stripe

```sh
npx -y @salesdogg/cli apps create --provider stripe --credentials.secret_key "$STRIPE_SECRET_KEY"
npx -y @salesdogg/cli apps import-products <app_id>
```

When the API is reachable over public HTTPS, SalesDogg registers its webhook endpoint in your Stripe account itself. Renewals and trial conversions are recorded when the invoice is paid (`invoice.paid`), not when Stripe rolls the period over, so a declined renewal is a `billing_issue` and never a `renewal`. Refunds of subscription charges are recorded as `refund` events with the amount; Stripe keeps the subscription running, so access is unchanged.

Sell with a hosted checkout attributed to your user:

```sh
npx -y @salesdogg/cli checkout create --app-user-id user_1 --product-id <product_id> --success-url https://example.com/ok
```

Developing against a local API? Forward Stripe's events with the Stripe CLI; the exact `stripe listen` command is in the app's `webhook_instructions`.

## Braintree

```sh
npx -y @salesdogg/cli apps create --provider braintree --credentials.merchant_id ... --credentials.public_key ... --credentials.private_key ... --credentials.environment sandbox
```

`environment` is `sandbox` (default) or `production`. Cancelled subscriptions expire at the paid-through date through a background sweep.

## App Store

```sh
npx -y @salesdogg/cli apps create --provider app_store --config.bundle_id com.example.app
```

Point App Store Server Notifications V2 at the app's `webhook_url`. Notifications are verified against Apple's root certificates. Register products with their App Store product ids. To look purchases up at Apple (needed by the RevenueCat import), add an In-App Purchase key: `issuer_id`, `key_id` and `private_key`.

## Google Play

```sh
npx -y @salesdogg/cli apps create --provider play_store --config.package_name com.example.app --credentials.service_account_json "$(cat service-account.json)"
```

Real-time developer notifications arrive through a Pub/Sub push subscription to the app's `webhook_url`. Purchases are acknowledged automatically. Register subscription products as `productId:basePlanId` (for example `pro:monthly`) and one-time products as `productId`.

## Going live

The Free plan is sandbox only: Stripe test mode, the Braintree sandbox, App Store sandbox and TestFlight purchases, Google Play license testers and the simulator. Connecting live credentials needs a paid plan:

- a live Stripe key (`sk_live_…` or `rk_live_…`),
- a Braintree account with `environment: production`,
- an App Store app with `app_apple_id`, which is what lets Apple's production notifications in.

On Free, those requests answer `402` with `type: payment_required`, `code: plan_required`, and a link in `error.details.upgrade_url`:

```json
{
  "error": {
    "type": "payment_required",
    "code": "plan_required",
    "message": "Live Stripe keys need a paid plan. This organization is on Free, which is sandbox only.",
    "hint": "Show the user this link to choose a plan and pay (no sign-in needed): https://… . When they have paid, run the same request again. Test keys (sk_test_...) work on Free.",
    "details": { "upgrade_url": "https://…/upgrade/…", "plan": "free", "plans": [ … ] }
  }
}
```

The link opens a page where a person chooses Starter or Pro, monthly or annually, and pays with Stripe Checkout. It needs no sign-in, it can only be used to pay for that organization, and it is valid for 7 days. Agents should show it to the user, wait until they have paid, then repeat the request. `npx -y @salesdogg/cli billing get` returns the current plan and a fresh link at any time.

Google Play cannot be told apart at setup: a live Play purchase on Free is recorded as usual (your customer keeps access) and `billing get` shows `live_data_since`, the moment real payments started arriving.

## Receipts from your app

Mobile SDKs report purchases with `POST /v1/receipts` using the public key; the server verifies them with the store and returns the user's entitlements. When a receipt that belongs to one signed-in user is submitted for another, the project's restore behavior decides: `transfer` (default) moves it and records a `transfer` event; `keep_with_original` refuses with 409 `receipt_already_in_use`. Change it with `npx -y @salesdogg/cli settings update --restore-behavior keep_with_original`.

## Adding a provider

Implement `ProviderAdapter` in `apps/api/src/providers/types.ts`: verify the provider's notifications and translate them into state snapshots. Event detection, attribution, idempotency and entitlements are shared.
