# SalesDogg: the whole site --- --- > SalesDogg is a subscription and in-app purchase backend. It keeps one record of what each of your customers bought across Stripe, Braintree, the App Store and Google Play, tells your app who has access right now, and notifies your backend when that changes. It is free in sandbox (test mode, store sandboxes and the simulator) and a simple monthly price once you take real payments (Starter from $19, Pro from $49, billed annually), never a percentage of revenue: SalesDogg is not a payment provider and never touches the money. Everything can be set up by a person in the dashboard or by a coding agent through the API, CLI and MCP server. --- --- # SalesDogg: Subscriptions, without a cut of your revenue. > SalesDogg is a subscription and in-app purchase backend. It keeps one record of what each of your customers bought across Stripe, Braintree, the App Store and Google Play, tells your app who has access right now, and notifies your backend when that changes. It is free in sandbox (test mode, store sandboxes and the simulator) and a simple monthly price once you take real payments (Starter from $19, Pro from $49, billed annually), never a percentage of revenue: SalesDogg is not a payment provider and never touches the money. Everything can be set up by a person in the dashboard or by a coding agent through the API, CLI and MCP server. SalesDogg runs the subscription side of your app on Stripe, the App Store and Google Play. Free while you build and test, a simple monthly price when you go live. We don't want a percent of your success. We want happy customers. Works with Stripe, Braintree, App Store, Google Play. ## We're not a payment provider. So we don't take a cut. Stripe, Apple and Google move your money, so they charge on every payment. SalesDogg never touches your money, so charging a share of your revenue wouldn't be fair. You pay a simple monthly price, and we earn it by keeping you happy. - **Your growth is yours.** Raise your prices, launch in a new country, have a great month. Your SalesDogg bill doesn't change because you did well. - **A price you can budget.** One line on your expenses, known in advance. No invoice that jumps after a good launch. - **Free while you build.** Everything works in sandbox for free, with no limits. You or your agent can test the whole integration before you pay anything. ## Everything you need to sell subscriptions - **One record per customer.** Whether someone paid on the web with Stripe or in your iPhone app, it's the same customer with the same subscription. Your code never asks where they paid. - **Instant access checks.** Ask whether a user has Pro and get one answer, with trials, grace periods, cancellations and refunds already worked out. - **Told when anything changes.** New purchase, renewal, failed payment, cancellation, refund. Your backend hears about each one, signed, and retried for hours if your server is down. - **Revenue you can see.** MRR, active subscriptions, trials, conversions, churn and refunds, day by day, in a dashboard that stays out of your way. - **Test without a store.** The simulator runs a subscription through renewals, failed payments, refunds and expiry with no store account and no real money. - **Native SDKs.** Swift for iPhone, iPad and Mac, Kotlin for Android, and a JavaScript client for your server. Anonymous users carry their purchases when they sign in. ## Or let your coding agent set it up. Ask Claude Code, Codex or Cursor to add subscriptions. It reads our docs, connects your payment provider, defines what paying customers unlock and tests the whole thing before any real charge. You approve the sign-in; it does the rest. Agents: the setup prompt, the steps and MCP configuration are at https://salesdogg.com/agents.md. ## Coming from RevenueCat? Bring everything with you. One import moves your products, offerings and every customer with their history and attributes. Endpoints and webhooks that speak RevenueCat's format keep your existing backend working while you switch. - Products, entitlements and offerings - Customers, aliases and attributes - RevenueCat-compatible endpoints and webhooks ## Plans - **Free** ($0): Build and test everything before you go live. - **Starter** ($19 / month, billed annually ($228 a year), or $24 / month, billed monthly): Go live and take real payments. - **Pro** ($49 / month, billed annually ($588 a year), or $59 / month, billed monthly): For growing products. No limits. - **Enterprise** (Custom): For security reviews, data teams and procurement. Details: https://salesdogg.com/pricing.md ## Questions ### Why don't you charge a percentage of revenue? Because we're not a payment provider. SalesDogg never touches your money, so a share of your revenue wouldn't be fair. We don't want a percent of your success; we want you happy enough to stay. ### What does SalesDogg do? It keeps track of what each of your customers bought, wherever they paid (Stripe, Braintree, the App Store or Google Play), answers whether a user has access to a feature right now, and tells your backend whenever that changes. ### When do I start paying? When you go live. Building and testing in sandbox is free with no limits; taking real payments starts at $19 a month for Starter, billed annually. ### Who holds the money? Your payment providers do. Stripe, Braintree, Apple and Google charge your customers and pay you exactly as they do today. SalesDogg never touches the money; it keeps the record. ### Can my coding agent set it up? Yes. Every part of SalesDogg works from the command line and as tools for agents like Claude Code, Codex and Cursor. Two steps need you: approving the sign-in in your browser, and, when you go live, paying for a plan through the link your agent shows you. No separate sign-in for that one. ### Do I need a store account to try it? No. The simulator creates test subscriptions and runs them through renewals, failed payments, cancellations, refunds and expiry, with real events, without any provider connected. --- # Set up SalesDogg from your coding agent > Paste one prompt into Claude Code, Codex, Cursor or any agent that runs commands. It signs in, creates a project, connects your payment provider and proves the setup with the simulator. You approve the sign-in once. ## The prompt ```text Set up SalesDogg (subscriptions and in-app purchases) in this project. 1. Read https://salesdogg.com/llms.txt to learn the product, then https://salesdogg.com/docs/getting-started.md. 2. Run `npx -y @salesdogg/cli login`. It prints a link and a code: show them to me and wait while I approve. 3. Run `npx -y @salesdogg/cli init` to create a project. It writes SALESDOGG_SECRET_KEY and SALESDOGG_PUBLIC_KEY to .env; never print the secret key. 4. Ask me which provider we use (Stripe, Braintree, App Store or Google Play) and connect it with `npx -y @salesdogg/cli apps create`. Read provider keys from .env rather than asking me to paste them. Then import its products. 5. Create an entitlement for the paid features and prove it works with the simulator (`npx -y @salesdogg/cli simulator purchase`) before any real payment. 6. Add the access check and a webhook handler to our code with @salesdogg/sdk. 7. When I'm ready to go live, connect the live keys. The Free plan is sandbox only: if SalesDogg answers with error.type "payment_required", show me error.details.upgrade_url (a link to choose a plan and pay), wait until I say I've paid, then run the same command again. Every command prints JSON. If one fails, read error.hint and follow it. ``` ## What the agent does 1. **Sign in** (needs a person). The CLI starts a device sign-in and prints a link and a short code. You open the link, sign in or create an account, and approve the code. New accounts confirm their e-mail address first. The agent never sees your password. ```sh npx -y @salesdogg/cli login ``` 2. **Create a project**. Creates the project and its keys and appends SALESDOGG_API_URL, SALESDOGG_SECRET_KEY and SALESDOGG_PUBLIC_KEY to ./.env. ```sh npx -y @salesdogg/cli init ``` 3. **Turn on payments**. Connects your provider. For Stripe that is a secret key (start with a test-mode key); SalesDogg registers the webhook itself when the API is public. The response lists any manual step, such as pasting a notification URL into App Store Connect. ```sh npx -y @salesdogg/cli apps create --provider stripe --credentials.secret_key "$STRIPE_SECRET_KEY" ``` 4. **Import products and define access**. Pulls the provider's products and attaches the paid ones to an entitlement. ```sh npx -y @salesdogg/cli apps import-products npx -y @salesdogg/cli entitlements create --lookup-key pro --product-ids ``` 5. **Prove it with the simulator**. Buys, renews, fails a payment and expires a sandbox subscription, checking access at each step, with no real charge. ```sh npx -y @salesdogg/cli simulator purchase --app-user-id test_user --product-id npx -y @salesdogg/cli customers entitlements test_user ``` 6. **Go live** (needs a person). When you're ready for real payments, the agent connects your live keys. On the Free plan SalesDogg answers with a link to choose a plan and pay, ready to use with no sign-in. The agent shows it to you; once you've paid, it runs the same command again. ```sh npx -y @salesdogg/cli billing get ``` 7. **Wire it into your code**. Adds the access check where paid features live, a checkout link or the mobile SDK for buying, and a webhook endpoint that verifies the SalesDogg-Signature header. ```sh npx -y @salesdogg/cli webhooks create --url https://example.com/webhooks/salesdogg ``` ## MCP server setup `npx -y @salesdogg/cli mcp` serves every API operation as a tool over stdio. It needs no key in the config: after `login` and `init` it uses the saved sign-in, or SALESDOGG_API_KEY from its environment. ### Claude Code Terminal: ```sh claude mcp add salesdogg -- npx -y @salesdogg/cli mcp ``` Add --scope project to share it with your team through .mcp.json. ### OpenAI Codex Terminal: ```sh codex mcp add salesdogg -- npx -y @salesdogg/cli mcp ``` Or add [mcp_servers.salesdogg] with command = "npx" and args = ["-y", "@salesdogg/cli", "mcp"] to ~/.codex/config.toml. ### Cursor .cursor/mcp.json: ```json { "mcpServers": { "salesdogg": { "command": "npx", "args": [ "-y", "@salesdogg/cli", "mcp" ] } } } ``` ### VS Code (GitHub Copilot) .vscode/mcp.json: ```json { "servers": { "salesdogg": { "type": "stdio", "command": "npx", "args": [ "-y", "@salesdogg/cli", "mcp" ] } } } ``` ### Windsurf ~/.codeium/windsurf/mcp_config.json: ```json { "mcpServers": { "salesdogg": { "command": "npx", "args": [ "-y", "@salesdogg/cli", "mcp" ] } } } ``` ### Gemini CLI ~/.gemini/settings.json: ```json { "mcpServers": { "salesdogg": { "command": "npx", "args": [ "-y", "@salesdogg/cli", "mcp" ] } } } ``` ### Claude Desktop claude_desktop_config.json: ```json { "mcpServers": { "salesdogg": { "command": "npx", "args": [ "-y", "@salesdogg/cli", "mcp" ] } } } ``` Claude Desktop cannot run the sign-in itself: run `npx -y @salesdogg/cli login` and `npx -y @salesdogg/cli init` in a terminal first. ## Machine-readable resources - [/llms.txt](https://salesdogg.com/llms.txt): Index of the site for language models, with a link to every page's markdown. - [/llms-full.txt](https://salesdogg.com/llms-full.txt): The whole site, docs and API reference included, in one markdown file. - [/openapi.json](https://salesdogg.com/openapi.json): The OpenAPI 3.1 document the API, CLI and MCP server are all generated from. - [/docs/api.md](https://salesdogg.com/docs/api.md): Every endpoint with its CLI command, MCP tool, credentials and parameters. - [salesdogg commands](https://salesdogg.com/docs/cli): Prints every CLI command and its parameters as JSON. --- # Pricing Free in sandbox with no limits; a simple monthly price once you take real payments. SalesDogg never charges a percentage of your revenue. > SalesDogg is not generally available yet. These are the launch prices; nothing is charged until launch. ## Free: $0 Build and test everything before you go live. - Sandbox only: test mode, store sandboxes and the simulator - Unlimited test users and purchases - Stripe, Braintree, App Store and Google Play - Dashboard, webhooks, SDKs and agent tools - One project ## Starter: $19 / month, billed annually ($228 a year), or $24 / month, billed monthly Go live and take real payments. - Everything in Free - Live payments on every provider - More monthly active users - One project ## Pro: $49 / month, billed annually ($588 a year), or $59 / month, billed monthly For growing products. No limits. - Everything in Starter - Unlimited monthly active users - Unlimited projects - Import from RevenueCat ## Enterprise: Custom For security reviews, data teams and procurement. - Everything in Pro - Single sign-on (SSO) (planned, not available yet) - Export to your data warehouse (planned, not available yet) - Uptime SLA and a named contact ## We're not a payment provider. So we don't take a cut. Stripe, Apple and Google move your money, so they charge on every payment. SalesDogg never touches your money, so charging a share of your revenue wouldn't be fair. You pay a simple monthly price, and we earn it by keeping you happy. ## Questions ### Why don't you take a percentage of revenue? Because we're not a payment provider. Stripe, Apple and Google move your money, so they charge on each payment. SalesDogg never touches your money, so taking a share of your revenue wouldn't be fair. We don't want a percent of your success; we want you to be happy enough to stay. ### What can I do on the Free plan? Everything, in sandbox: Stripe test mode, the Braintree sandbox, App Store sandbox and TestFlight purchases, Google Play license testers and the simulator, with no limits. You or your agent can build and test the whole integration for free. Taking real payments needs Starter or above. ### How do I upgrade? When you or your agent connect live keys on the Free plan, SalesDogg answers with a link to choose Starter or Pro and pay. It works without signing in, so your agent can hand it straight to you. You can also get the link any time with salesdogg billing get. ### How does SalesDogg know a purchase is real? The payment provider tells us, and your app can't change it: Stripe marks every object live or test, Braintree sandbox is a separate account, Apple signs the environment into each transaction, and Google marks purchases by license testers as tests. ### What if a real purchase arrives while I'm on Free? It's recorded and your customer gets access, as they should. We'll ask you to move to a paid plan; nothing is dropped and nobody loses access. ### What is a monthly active user? An end user of your app that SalesDogg sees during a calendar month, for example when your app checks their access or records a purchase. Starter includes a set number; Pro has no limit. ### How much do I save by paying annually? At least two months a year. Starter is $19 a month billed annually ($228 a year) or $24 month to month; Pro is $49 a month billed annually ($588 a year) or $59 month to month. ### Does SalesDogg take anything from my payments? No. Your customers pay Stripe, Braintree, Apple or Google, who pay you as they do today. Their fees are between you and them. ### Do agents cost extra? No. The CLI and the agent tools use the same API as everything else. --- # Overview > What SalesDogg is, how it is put together, and where to start. SalesDogg is a subscription and in-app purchase backend. It sits between your product and the places people pay you (Stripe, Braintree, the App Store and Google Play) and answers one question reliably: **what does this user have access to right now?** Every provider is normalized into one model, one event stream and one API. Everything a person can do, an agent can do. The REST API, the CLI and the MCP server are generated from the same OpenAPI document, output is JSON, and every error carries a `hint` saying what to do next. ## Start here - [Getting started](/docs/getting-started): sign in, create a project, check access in five commands. - [Set up with your agent](/agents): one prompt for Claude Code, Codex, Cursor and other agents, plus MCP setup for each. - [Connect payment providers](/docs/payments): Stripe, Braintree, App Store and Google Play. - [API reference](/docs/api): every endpoint with its CLI command and MCP tool. ## How it fits together ``` provider notification ──► verify + translate ──► compare with stored state ──► events ──► your webhooks (or a receipt from your app) (renewal? billing issue?) │ access check ◄── entitlements of active subscriptions ``` Store notifications and receipts become provider-neutral snapshots. SalesDogg compares each snapshot with what it already knows to decide what happened (an initial purchase, a renewal, a billing issue, a cancellation), records an event and sends it to your webhook endpoints. A renewal only counts once it is paid, so a declined renewal is a billing issue and never a renewal. ## For language models This documentation is available as markdown: add `.md` to any page address, read [/llms.txt](/llms.txt) for an index, or [/llms-full.txt](/llms-full.txt) for everything in one file. The machine-readable API description is [/openapi.json](/openapi.json). --- # Getting started > Sign in, create a project, connect a provider and check a user's access, from the command line. You need Node 20 or later. Nothing else to install: every command below runs the CLI with `npx`. > Prefer to let your coding agent do this? Paste the prompt from [Set up with your agent](/agents) instead. ## 1. Sign in ```sh npx -y @salesdogg/cli login ``` This starts a device sign-in: the CLI prints a link and a short code, and opens the link if it can. Sign in (or create an account) in the browser and approve the code. New accounts confirm their e-mail address first. The session is saved in your user configuration, not in the project. For scripts there is a non-interactive form: `npx -y @salesdogg/cli login --email you@example.com --password '...'`, and `npx -y @salesdogg/cli signup` with the same flags to create an account. ## 2. Create a project ```sh npx -y @salesdogg/cli init ``` `init` creates a project named after the current directory and appends its keys to `./.env`: ```sh SALESDOGG_API_URL=https://api.salesdogg.com SALESDOGG_SECRET_KEY=sd_sk_... # servers, agents, CI: full access to this project SALESDOGG_PUBLIC_KEY=sd_pk_... # safe to ship in apps ``` From here on, the CLI finds the secret key in `.env` on its own. ## 3. Try it with the simulator The simulator creates sandbox purchases with no provider and nothing to configure. They go through the same path as real ones. ```sh npx -y @salesdogg/cli simulator purchase --app-user-id user_1 --store-identifier pro_monthly --trial-days 14 # note subscription.id and subscription.product_id in the output npx -y @salesdogg/cli entitlements create --lookup-key pro --product-ids npx -y @salesdogg/cli customers entitlements user_1 # "pro": true npx -y @salesdogg/cli simulator advance --action fail_payment # billing_issue npx -y @salesdogg/cli simulator advance --action expire # access removed ``` Actions: `renew`, `cancel`, `uncancel`, `fail_payment`, `recover`, `pause`, `resume`, `expire`, `refund`. Each call returns the events it produced. Real subscriptions cannot be changed through the simulator. ## 4. Connect a real provider ```sh npx -y @salesdogg/cli apps create --provider stripe --credentials.secret_key "$STRIPE_SECRET_KEY" npx -y @salesdogg/cli apps import-products npx -y @salesdogg/cli entitlements attach-products pro --product-ids ``` The response includes `webhook_url` and `webhook_instructions` for the one manual step a provider may need. See [Connect payment providers](/docs/payments) for Braintree, the App Store and Google Play. ## 5. Sell and check access ```sh npx -y @salesdogg/cli checkout create --app-user-id user_1 --product-id --success-url https://example.com/ok npx -y @salesdogg/cli customers entitlements user_1 ``` `checkout create` returns a Stripe-hosted checkout `url` attributed to `user_1`. In your backend, the same check is one call with the [JavaScript SDK](/docs/sdks): ```ts import { createClient } from "@salesdogg/sdk"; const sd = createClient({ apiKey: process.env.SALESDOGG_SECRET_KEY! }); if (await sd.hasEntitlement(user.id, "pro")) { // unlock } ``` ## Next - [Events and webhooks](/docs/webhooks): learn when access changes. - [Mobile SDKs](/docs/sdks): StoreKit 2 and Play Billing. - [CLI and MCP server](/docs/cli): every command, and how agents load them as tools. --- # Concepts > Organizations, projects, apps, products, entitlements, offerings, customers and events. | Concept | Meaning | | --- | --- | | Organization / project | Tenancy. People belong to organizations; a project holds all data for one product. API keys are bound to one project. | | App | A connection to one provider account or store listing. Carries the webhook URL you give the provider. | | Product | Something sold in a store: a Stripe price, a Braintree plan, an App Store or Play product. | | Entitlement | A level of access, such as `pro`. Unlocked by any attached product, or granted manually. | | Offering / package | The products you present to customers, fetched by client apps to build a paywall. | | Customer | A user, identified by your own `app_user_id`. Earlier ids (an anonymous id merged on sign-in, ids imported from RevenueCat) stay attached as aliases and find the same customer. | | Subscription / purchase | Normalized state of what a customer bought, whichever provider sold it. | | Event | `initial_purchase`, `renewal`, `cancellation`, `billing_issue`, `expiration`, `refund` and more. Sent to your webhook endpoints. | ## The access rule A subscription grants its entitlements while its status is `trialing`, `active` or `in_grace_period`. A cancelled subscription keeps access until the paid period ends. Manual grants add access with an optional expiry. A period only counts once it is paid. Stripe, for example, rolls a subscription into its next period about an hour before charging; SalesDogg keeps the last paid period (and keeps a trial a trial) until the invoice is paid. ## Environments Every subscription and purchase is either `production` or `sandbox`. Simulator purchases, Stripe test mode and store sandboxes are `sandbox`. Metrics and lists take an `environment` filter. ## Conventions - Errors are always `{ "error": { type, code, message, param?, hint? } }`. `hint` says what to do next. - Creates keyed by `lookup_key`, `app_user_id` or `store_identifier` are idempotent upserts, so retries are safe. - Lists take `limit` and `starting_after`; responses carry `has_more` and `next_cursor`. - Money is in minor units (cents). Timestamps are ISO 8601 in UTC. - Ids are prefixed and time-sortable: `cus_…`, `sub_…`, `evt_…`. --- # 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 ``` 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 --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. --- # Customers and attributes > Anonymous and signed-in users, aliases, merging, customer attributes, last-seen details and search. Customer tracking follows RevenueCat's model, so apps and data move over unchanged. ## Identity SDKs start with an anonymous id (`$anon:`; RevenueCat's `$RCAnonymousID:` is accepted too). When your user signs in, `identify` moves the anonymous customer's purchases and attributes to the signed-in user. The anonymous id lives on as an alias, so any endpoint called with it finds the user. `original_app_user_id` is the first id the customer had. ```sh npx -y @salesdogg/cli customers identify --anonymous-id '$anon:6f1c…' --app-user-id user_1 ``` ## Attributes - String values up to 500 characters; `null` or `""` deletes one. - Custom keys start with a letter (letters, digits, `_`, `-`), at most 40 characters and 50 keys per customer. - `$` keys are reserved: `$email`, `$displayName`, `$phoneNumber`, campaign attribution (`$mediaSource`, `$campaign`, `$adGroup`, `$ad`, `$keyword`, `$creative`) and integration ids. - Each write may carry `updated_at_ms`; an older write never replaces a newer one. - Apps write attributes with the public key but cannot read them back, and campaign attribution set from an app cannot be overwritten by the app. ```ts await sd.setAttributes(user.id, { $email: user.email, plan_tier: "gold" }); ``` Anyone holding your public key can write attributes for any user id, as with RevenueCat, so never base access decisions on attributes. ## Last seen SDK requests send `X-Platform`, `X-Platform-Version`, `X-App-Version` and `X-Locale`; the customer keeps the latest, plus a country taken from the locale. A server-side access check with a secret key does not count as the user being seen unless it sends `X-Platform`. ## Search `GET /v1/customers` (`npx -y @salesdogg/cli customers list`) filters by `search` (id or alias prefix, exact e-mail), `entitlement`, `active`, `platform`, `country`, `app_version`, `store`, `attribute_key` / `attribute_value`, and first- or last-seen ranges. The dashboard exports any filtered list as CSV. ## Grants Give access without a purchase, for support cases, partners or tests: ```sh npx -y @salesdogg/cli customers grant user_1 --entitlement pro --expires-at 2026-12-31T00:00:00Z npx -y @salesdogg/cli customers revoke-grant user_1 pro ``` --- # Events and webhooks > Every lifecycle event SalesDogg records, how webhooks are signed and retried, and how to verify them. Every change to a customer's purchases is recorded as an **event** and sent to your **webhook endpoints**. List them with `npx -y @salesdogg/cli events list --app-user-id user_1`. ## Event types | Type | Meaning | | --- | --- | | `initial_purchase` | First paid period of a subscription. | | `trial_started` | A free trial began. | | `trial_converted` | The first payment after a trial went through. | | `renewal` | A new period was paid. | | `cancellation` | Auto-renew was turned off. Access continues until the paid period ends. | | `uncancellation` | Auto-renew was turned back on. | | `billing_issue` | A renewal payment failed. | | `billing_recovered` | A failed renewal was paid after all. | | `paused` | The subscription was paused. | | `resumed` | A paused subscription resumed. | | `product_change` | The customer moved to another product. | | `expiration` | The subscription ended and its access was removed. | | `refund` | A payment was refunded. | | `non_renewing_purchase` | A one-time purchase. | | `transfer` | A purchase moved to another customer (restore behavior `transfer`). | | `grant_created` | Access was granted without a purchase. | | `grant_revoked` | A grant was revoked. | | `test` | Sent by `webhooks test`. | ## Register an endpoint ```sh npx -y @salesdogg/cli webhooks create --url https://example.com/webhooks/salesdogg npx -y @salesdogg/cli webhooks test ``` The response includes the endpoint's signing `secret`, shown once. Endpoints must be public `https` URLs; addresses inside private networks are refused, and redirects are not followed. Pass `--event-types` to receive only some events. `--format revenuecat` sends RevenueCat's webhook body instead of SalesDogg's event object, for receivers written for RevenueCat; add `--authorization-header` to send a fixed `Authorization` header. Events with no RevenueCat counterpart (`billing_recovered`, grants) are not sent in that format. ## Verify the signature Each delivery carries a `SalesDogg-Signature` header: `t=,v1=.">`, keyed with the endpoint's secret. Verify it against the raw body, before parsing: ```ts import { verifyWebhook } from "@salesdogg/sdk"; export async function POST(request: Request) { const event = await verifyWebhook( await request.text(), request.headers.get("salesdogg-signature"), process.env.SALESDOGG_WEBHOOK_SECRET!, ); // throws if the signature is wrong or older than 5 minutes // handle event.type return new Response(null, { status: 200 }); } ``` ## Delivery and retries Respond with a 2xx status. A failed delivery is retried up to 8 attempts in all, with the wait growing from 30 seconds and capped at 24 hours. `npx -y @salesdogg/cli webhooks deliveries` lists attempts and their results. Deliveries are claimed with row locks, so with several API instances each one is still sent once. Handle events idempotently by `id`: a retry after a timeout can deliver an event your handler already processed. --- # SDKs > The JavaScript client for servers, and the Swift (StoreKit 2) and Kotlin (Play Billing) SDKs for apps. ## JavaScript and TypeScript A dependency-free client for servers and edge runtimes, plus webhook verification. ```sh npm install @salesdogg/sdk ``` ```ts import { createClient } from "@salesdogg/sdk"; const sd = createClient({ apiKey: process.env.SALESDOGG_SECRET_KEY! }); await sd.hasEntitlement(user.id, "pro"); // boolean await sd.entitlements(user.id); // active entitlements await sd.setAttributes(user.id, { $email: user.email }); const { url } = await sd.createCheckout({ app_user_id: user.id, product_id: "prod_…", success_url: "https://example.com/ok" }); const { data } = await sd.listCustomers({ entitlement: "pro", country: "DE" }); ``` Errors are thrown as `SalesDoggError` with the API's `status`, `type`, `code` and `hint`. Set `apiUrl` for a local or self-hosted API. ## Mobile SDKs Native SDKs for Apple platforms and Android share one small API and behave the same way: - **Anonymous first.** Without a user id the SDK creates `$anon:` and keeps it across launches. `identify(userId)` moves anything bought anonymously to the user; `signOut()` starts a new anonymous id. - **The server decides access.** After a purchase the SDK sends the store's proof to `POST /v1/receipts`; entitlements come back from the server, never from the device. - **No lost purchases.** If recording fails (offline, server down) the purchase is retried on the next launch. - **Works offline.** The last known entitlements are cached and returned with `isStale = true` when the server is unreachable. - **Background changes.** Renewals, Ask to Buy approvals and pending payments that clear are published on the entitlement stream. Both use the **public key** (`sd_pk_…`). ### Swift (iOS, macOS, tvOS, watchOS) StoreKit 2, Swift concurrency, no dependencies. iOS 15+, macOS 12+, tvOS 15+, watchOS 8+. Add the package with Swift Package Manager and pick the `SalesDogg` library. ```swift import SalesDogg SalesDogg.configure(publicKey: "sd_pk_...") let offering = try await SalesDogg.shared.offerings().current switch try await SalesDogg.shared.purchase(offering!.packages[0]) { case .purchased(let entitlements): unlock(entitlements.isActive("pro")) case .pending: showPendingNotice() case .cancelled: break } try await SalesDogg.shared.identify(user.id) // after your own sign-in ``` Server side: an `app_store` app with your bundle id, and products registered with their App Store product ids. If your user ids are lowercase UUIDs, the SDK also passes them to the App Store as `appAccountToken`, so notifications are attributed even if the app is killed mid-purchase. ### Kotlin (Android) Google Play Billing Library 9 and coroutines, minSdk 23. Two modules: `salesdogg-core` (pure Kotlin) and `salesdogg` (Play Billing). ```kotlin import com.salesdogg.sdk.* import com.salesdogg.sdk.android.* SalesDogg.configure(this, publicKey = "sd_pk_...") // Application.onCreate val offering = SalesDogg.shared.offerings().current when (val outcome = SalesDogg.shared.purchase(activity, offering!!.packages.first())) { is PurchaseOutcome.Purchased -> unlock(outcome.entitlements.isActive("pro")) PurchaseOutcome.Pending -> showPendingNotice() PurchaseOutcome.Cancelled -> Unit } lifecycleScope.launch { SalesDogg.shared.entitlementUpdates.collect { render(it) } } ``` Server side: a `play_store` app with your package name and service account. Purchases are acknowledged by the SalesDogg server, so the app does not call `acknowledgePurchase`. Consumable products are not supported yet. ### Coming from RevenueCat Pass RevenueCat's current app user id to `configure` the first time only. An anonymous `$RCAnonymousID:` id is treated as anonymous, so that user's purchases follow them when they sign in. --- # CLI and MCP server > The salesdogg command line, how it finds credentials, and how agents load every operation as MCP tools. The CLI has one command per API operation, generated at runtime from the OpenAPI document. Output is always JSON on stdout. Errors are JSON on stderr with a non-zero exit code, and `error.hint` says what to do next. ```sh npx -y @salesdogg/cli --help # or install it: npm install -g @salesdogg/cli salesdogg --help # actions in a group salesdogg --help # one command's parameters salesdogg commands # every command and parameter, as JSON ``` ## Getting started commands | Command | What it does | | --- | --- | | `salesdogg login` | Sign in through the browser (device code), or `--email` / `--password` for non-interactive use | | `salesdogg signup` | Create an account: `--email --password [--name]` | | `salesdogg init` | Create a project and append its keys to `./.env` (signs you in first if needed) | | `salesdogg status` | Show which credentials and project are in use | | `salesdogg use ` | Choose the project for session-authenticated commands | | `salesdogg api ` | Call any endpoint directly; `--data '{...}'` for a JSON body | | `salesdogg import revenuecat` | Move from RevenueCat (see [Moving from RevenueCat](/docs/revenuecat)) | | `salesdogg mcp` | Run the MCP server over stdio | | `salesdogg billing get` | Your plan, and on Free a link to choose a plan and pay | Every API operation is `salesdogg `; the [API reference](/docs/api) lists the command beside each endpoint. Flags are the parameter names with `-` for `_`; nested objects take `--credentials.secret_key value` or JSON. ## Credentials In order of precedence: 1. `SALESDOGG_API_KEY` (or `SALESDOGG_SECRET_KEY`) in the environment or `./.env`. 2. The session saved by `salesdogg login`, plus the project chosen with `salesdogg init` or `salesdogg use`. `SALESDOGG_API_URL` selects the server (default `https://api.salesdogg.com`). ## MCP server `salesdogg mcp` serves every API operation as a tool over stdio. Tool names are `_`, for example `customers_entitlements`. Read operations are marked read-only and deletes destructive, so clients can ask before running them. ```json { "mcpServers": { "salesdogg": { "command": "npx", "args": ["-y", "@salesdogg/cli", "mcp"] } } } ``` It takes credentials the same way as the CLI. To pin a project, add `"env": { "SALESDOGG_API_KEY": "sd_sk_..." }`. Setup for each agent is on [Set up with your agent](/agents). --- # Keys and roles > Secret keys, public keys, sessions, and what each organization role may do. Send `Authorization: Bearer ` with every request. | Credential | Use | Can do | | --- | --- | --- | | Secret key `sd_sk_…` | Servers, agents, CI | Everything in one project | | Public key `sd_pk_…` | Shipped in client apps | Read offerings, check a user's entitlements, submit App Store and Google Play receipts, attach an anonymous user to a signed-in one, set (not read) customer attributes | | Session | People (`salesdogg login`, the dashboard) | Projects of their organizations, according to their role. Project-scoped endpoints need an `X-Project-Id` header. | A public key can be extracted from any app, so it never returns a customer's attributes or purchase history, and it cannot link a Stripe or Braintree subscription by id (that id is not proof of purchase; use a secret key, or attribute the purchase at checkout). ## Keys ```sh npx -y @salesdogg/cli api-keys list npx -y @salesdogg/cli api-keys create --kind secret --name ci npx -y @salesdogg/cli api-keys revoke ``` A secret key is shown once, when it is created. Keep it in your environment or a secret manager, never in an app or a repository. ## Organization roles Roles apply to signed-in people. Owners and admins can do everything. Members can view and operate (catalog, customers, grants, simulator) but cannot create or revoke API keys, connect, change or remove providers, add or remove webhook endpoints, delete customers, or create or delete projects. A secret key carries its project's full authority regardless of who created it. ## Rate limits 600 requests a minute per secret key or user, and 120 per client address for public keys. Responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`; a 429 includes `Retry-After`. Provider webhooks are never limited. --- # Moving from RevenueCat > Import your catalog and customers, keep RevenueCat-format endpoints and webhooks working, and switch SDKs. ## 1. Connect your stores Create an app for each store (`npx -y @salesdogg/cli apps create ...`, see [Connect payment providers](/docs/payments)) and point their server notifications at SalesDogg. For the App Store, add an In-App Purchase key (`issuer_id`, `key_id`, `private_key`) so purchases can be looked up at Apple. ## 2. Import ```sh npx -y @salesdogg/cli import revenuecat --revenuecat-key "$REVENUECAT_API_KEY" --revenuecat-project ``` This imports products, entitlements and offerings, then every customer: app user ids and aliases, attributes, last-seen details and promotional entitlements. It is safe to run again, and `--starting-after` resumes. The RevenueCat key (a v2 secret key) is used for the request only and never stored. The same import is in the dashboard under Settings, and as two endpoints: `POST /v1/imports/revenuecat/catalog` and `POST /v1/imports/revenuecat/customers`. ## 3. Purchases Purchases are not copied from RevenueCat: their ids are not the ones store notifications use. Each is looked up at its store and recorded exactly as a store notification would be (Stripe through its API, Google Play through the Orders API, the App Store with the key above). Anything that cannot be looked up is linked by the SDK the next time the app opens, because it syncs the device's purchases at launch. Nothing imported sends webhooks. ## 4. Backend code These endpoints answer in RevenueCat's REST v1 format; change the base URL and key: - `GET /v1/subscribers/{app_user_id}` - `DELETE /v1/subscribers/{app_user_id}` - `POST /v1/subscribers/{app_user_id}/attributes` - `POST /v1/subscribers/{app_user_id}/entitlements/{entitlement}/promotional` - `POST /v1/subscribers/{app_user_id}/entitlements/{entitlement}/revoke_promotionals` Webhook endpoints created with `--format revenuecat` (and optionally `--authorization-header`) receive RevenueCat's webhook body. ## 5. Apps Replace the RevenueCat SDK with SalesDogg's. Pass RevenueCat's current app user id to `configure` the first time, so anonymous users' purchases stay attached. ## Not covered Amazon, Roku, Paddle and RevenueCat Web Billing purchases; RevenueCat's own prices (SalesDogg learns prices from store notifications, so MRR for imported subscriptions fills in as they renew); experiments and paywalls. `grace_period_expires_date` is always null in the RevenueCat format. --- # SalesDogg API reference Version 0.1.0. Generated from https://salesdogg.com/openapi.json, the document the CLI and the MCP server are generated from. Each operation lists its CLI command and MCP tool name. SalesDogg is a subscription and in-app purchase backend. It normalizes Stripe, Braintree, App Store and Google Play into one model: **customers** own **subscriptions** and **purchases** of **products**; products unlock **entitlements**; **offerings** group products for display. ## Authentication Send `Authorization: Bearer `. - **Secret key** (`sd_sk_...`): full access to one project. Server-side only. - **Public key** (`sd_pk_...`): safe in client apps; read offerings, check a user's entitlements, submit App Store / Google Play receipts, attach an anonymous user to a signed-in one, set customer attributes. It never returns attributes or purchase history. - **Session token** (from `salesdogg login`): a human user. Project-scoped endpoints then need an `X-Project-Id` header. Some operations need the owner or admin role in the organization; each endpoint says so. ## Try it without a store account `POST /v1/simulator/purchases` with `{ "app_user_id": "user_1", "store_identifier": "pro_monthly" }` creates a sandbox subscription with no setup. Attach its product to an entitlement, check the user's access, then drive renewals, cancellations, billing issues and refunds with `POST /v1/simulator/subscriptions/{id}/advance`. Events and webhooks behave as in production. ## Quickstart 1. `POST /v1/apps` – connect a provider. 2. `POST /v1/apps/{app_id}/import_products` (or `POST /v1/products`). 3. `POST /v1/entitlements` with `product_ids`. 4. `POST /v1/checkout_sessions` (web) or `POST /v1/receipts` (mobile) to record purchases. 5. `GET /v1/customers/{app_user_id}/entitlements` to check access. ## Moving from RevenueCat - `POST /v1/imports/revenuecat/catalog`, then `POST /v1/imports/revenuecat/customers` page by page (or `salesdogg import revenuecat`). - `GET /v1/subscribers/{app_user_id}` and the other `/v1/subscribers` endpoints answer in RevenueCat's REST v1 format. - Webhook endpoints with `format: "revenuecat"` receive RevenueCat's webhook body. - Customer attributes, aliases, `$RCAnonymousID:` ids and restore behavior (`GET/PATCH /v1/settings`) follow RevenueCat's rules. ## Conventions - Errors: `{ "error": { type, code, message, param?, hint? } }`. `hint` says what to do next. - Creates keyed by `lookup_key`, `app_user_id` or `store_identifier` are idempotent upserts and safe to retry. - Lists: `limit` + `starting_after`; responses carry `has_more` and `next_cursor`. - Money is in minor units (cents). Timestamps are ISO 8601 UTC. - Rate limits: responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds). A 429 includes `Retry-After`. ## Account ### Identify the current credential `GET /v1/me` - CLI: `salesdogg account whoami` - MCP tool: `account_whoami` - Credentials: Session, Secret key Returns the user (session) or the project (API key) the credential belongs to. Useful to verify setup. ## Organizations ### List organizations you belong to `GET /v1/organizations` - CLI: `salesdogg organizations list` - MCP tool: `organizations_list` - Credentials: Session ## Projects ### List projects you can access `GET /v1/projects` - CLI: `salesdogg projects list` - MCP tool: `projects_list` - Credentials: Session ### Create a project `POST /v1/projects` - CLI: `salesdogg projects create` - MCP tool: `projects_create` - Credentials: Session A project holds all billing data for one product. If you have no organization yet, one is created. The response includes a secret and a public API key so you can start immediately. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `name` | body | string | yes | | | `organization_id` | body | string | no | Defaults to your only organization, or creates one named after the project. | ### Delete a project and all of its data `DELETE /v1/projects/{project_id}` - CLI: `salesdogg projects delete ` - MCP tool: `projects_delete` - Credentials: Session Needs the owner or admin role in the organization. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `project_id` | path | string | yes | | ## API keys ### List API keys of the project `GET /v1/api_keys` - CLI: `salesdogg api-keys list` - MCP tool: `api_keys_list` - Credentials: Session, Secret key ### Create an API key `POST /v1/api_keys` - CLI: `salesdogg api-keys create` - MCP tool: `api_keys_create` - Credentials: Session, Secret key (sessions need the owner or admin role) Secret keys (sd_sk_) have full access to the project and must stay on servers. Public keys (sd_pk_) are safe to ship in client apps: they can only read offerings, check a user's entitlements, submit App Store and Google Play receipts, attach an anonymous user to a signed-in one, and set (not read) customer attributes. Signed-in users need the owner or admin role in the organization. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `kind` | body | "secret" \| "public" | yes | | | `name` | body | string | no | | ### Revoke an API key `DELETE /v1/api_keys/{api_key_id}` - CLI: `salesdogg api-keys revoke ` - MCP tool: `api_keys_revoke` - Credentials: Session, Secret key (sessions need the owner or admin role) Signed-in users need the owner or admin role in the organization. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `api_key_id` | path | string | yes | | ## Apps (provider connections) ### List provider connections `GET /v1/apps` - CLI: `salesdogg apps list` - MCP tool: `apps_list` - Credentials: Session, Secret key ### Connect a payment provider `POST /v1/apps` - CLI: `salesdogg apps create` - MCP tool: `apps_create` - Credentials: Session, Secret key (sessions need the owner or admin role) Creates a connection ("app") to one provider account or store listing. Required fields per provider: - stripe: credentials { secret_key, webhook_secret? }. When the API has a public HTTPS URL the webhook endpoint is registered in Stripe automatically. - braintree: credentials { merchant_id, public_key, private_key, environment }, config { customer_id_is_app_user_id? }. - app_store: config { bundle_id, app_apple_id? }, credentials { issuer_id, key_id, private_key } (optional: an In-App Purchase key, needed to look purchases up at Apple, e.g. for the RevenueCat import). - play_store: credentials { service_account_json }, config { package_name }. The response contains `webhook_url` and `webhook_instructions` describing the one remaining manual step, if any. The Free plan is sandbox only. Live credentials (a live Stripe key, Braintree `environment: production`, an App Store `app_apple_id`) on Free answer 402 `plan_required`; show the user `error.details.upgrade_url`, a link to choose a plan and pay, then retry. Signed-in users need the owner or admin role in the organization. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `provider` | body | "stripe" \| "braintree" \| "app_store" \| "play_store" \| "simulator" | yes | | | `name` | body | string | no | | | `credentials` | body | object | no | Provider secrets. Stored encrypted; never returned. | | `config` | body | object | no | Non-secret provider settings. | ### Get a provider connection `GET /v1/apps/{app_id}` - CLI: `salesdogg apps get ` - MCP tool: `apps_get` - Credentials: Session, Secret key | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_id` | path | string | yes | | ### Update a provider connection `PATCH /v1/apps/{app_id}` - CLI: `salesdogg apps update ` - MCP tool: `apps_update` - Credentials: Session, Secret key (sessions need the owner or admin role) Given credential and config fields are merged into the stored ones (e.g. set a Stripe `webhook_secret` after creating the endpoint). Signed-in users need the owner or admin role in the organization. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_id` | path | string | yes | | | `name` | body | string | no | | | `credentials` | body | object | no | | | `config` | body | object | no | | ### Delete a provider connection and its products `DELETE /v1/apps/{app_id}` - CLI: `salesdogg apps delete ` - MCP tool: `apps_delete` - Credentials: Session, Secret key (sessions need the owner or admin role) Signed-in users need the owner or admin role in the organization. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_id` | path | string | yes | | ### Import products from the provider `POST /v1/apps/{app_id}/import_products` - CLI: `salesdogg apps import-products ` - MCP tool: `apps_import_products` - Credentials: Session, Secret key Fetches the provider's catalog (Stripe prices, Braintree plans) and creates any products that do not exist yet. Idempotent. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_id` | path | string | yes | | ## Products ### List products `GET /v1/products` - CLI: `salesdogg products list` - MCP tool: `products_list` - Credentials: Session, Secret key | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `limit` | query | integer | no | Page size (1-100). | | `starting_after` | query | string | no | Cursor: the `next_cursor` value from the previous page. | | `app_id` | query | string | no | | ### Create a product `POST /v1/products` - CLI: `salesdogg products create` - MCP tool: `products_create` - Credentials: Session, Secret key Registers a store product so it can be attached to entitlements and offerings. Idempotent on (app_id, store_identifier): creating an existing product updates and returns it. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_id` | body | string | yes | | | `store_identifier` | body | string | yes | | | `type` | body | "subscription" \| "one_time" | no | | | `display_name` | body | string | no | | ### Delete a product `DELETE /v1/products/{product_id}` - CLI: `salesdogg products delete ` - MCP tool: `products_delete` - Credentials: Session, Secret key | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `product_id` | path | string | yes | | ## Entitlements ### List entitlements `GET /v1/entitlements` - CLI: `salesdogg entitlements list` - MCP tool: `entitlements_list` - Credentials: Session, Secret key ### Create or update an entitlement `POST /v1/entitlements` - CLI: `salesdogg entitlements create` - MCP tool: `entitlements_create` - Credentials: Session, Secret key An entitlement is a level of access (e.g. "pro") unlocked by any of its products. Idempotent on lookup_key. When product_ids is given it replaces the attached products. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `lookup_key` | body | string | yes | | | `display_name` | body | string | no | | | `product_ids` | body | string[] | no | | ### Attach products to an entitlement `POST /v1/entitlements/{entitlement}/products` - CLI: `salesdogg entitlements attach-products ` - MCP tool: `entitlements_attach_products` - Credentials: Session, Secret key | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `entitlement` | path | string | yes | Entitlement id or lookup_key. | | `product_ids` | body | string[] | yes | | ### Detach a product from an entitlement `DELETE /v1/entitlements/{entitlement}/products/{product_id}` - CLI: `salesdogg entitlements detach-product ` - MCP tool: `entitlements_detach_product` - Credentials: Session, Secret key | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `entitlement` | path | string | yes | Entitlement id or lookup_key. | | `product_id` | path | string | yes | | ### Delete an entitlement `DELETE /v1/entitlements/{entitlement}` - CLI: `salesdogg entitlements delete ` - MCP tool: `entitlements_delete` - Credentials: Session, Secret key | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `entitlement` | path | string | yes | Entitlement id or lookup_key. | ## Offerings ### List offerings `GET /v1/offerings` - CLI: `salesdogg offerings list` - MCP tool: `offerings_list` - Credentials: Session, Secret key, Public key Offerings are the sets of products you present to customers. Client apps fetch this with a public key and display the one with `is_current: true`. ### Create or update an offering `POST /v1/offerings` - CLI: `salesdogg offerings create` - MCP tool: `offerings_create` - Credentials: Session, Secret key Idempotent on lookup_key. When `packages` is given it replaces the offering's packages. The first offering of a project becomes current automatically. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `lookup_key` | body | string | yes | | | `display_name` | body | string | no | | | `is_current` | body | boolean | no | | | `metadata` | body | object | no | | | `packages` | body | object[] | no | | ### Get an offering `GET /v1/offerings/{offering}` - CLI: `salesdogg offerings get ` - MCP tool: `offerings_get` - Credentials: Session, Secret key, Public key | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `offering` | path | string | yes | Offering id or lookup_key. | ### Delete an offering `DELETE /v1/offerings/{offering}` - CLI: `salesdogg offerings delete ` - MCP tool: `offerings_delete` - Credentials: Session, Secret key | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `offering` | path | string | yes | Offering id or lookup_key. | ## Customers ### List and search customers `GET /v1/customers` - CLI: `salesdogg customers list` - MCP tool: `customers_list` - Credentials: Session, Secret key Newest first. Filters combine with AND. Each customer comes with attributes, aliases and last-seen device details. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `limit` | query | integer | no | Page size (1-100). | | `starting_after` | query | string | no | Cursor: the `next_cursor` value from the previous page. | | `search` | query | string | no | An app user id or alias (exact, or the start of one), or an exact $email address (case-insensitive). | | `entitlement` | query | string | no | Only customers with this entitlement (lookup_key) active right now. | | `active` | query | "true" \| "false" | no | true: customers with any active entitlement; false: customers with none. | | `platform` | query | string | no | Last seen platform, e.g. ios or android. | | `country` | query | string | no | Last seen country, ISO 3166-1 alpha-2. | | `app_version` | query | string | no | Last seen app version. | | `store` | query | "stripe" \| "braintree" \| "app_store" \| "play_store" \| "simulator" | no | Customers with a subscription or purchase from this store. | | `attribute_key` | query | string | no | With attribute_value: customers whose attribute equals the value (case-insensitive). Alone: customers that have the attribute. | | `attribute_value` | query | string | no | | | `first_seen_after` | query | string | no | | | `first_seen_before` | query | string | no | | | `last_seen_after` | query | string | no | | | `last_seen_before` | query | string | no | | ### Create or update a customer `POST /v1/customers` - CLI: `salesdogg customers upsert` - MCP tool: `customers_upsert` - Credentials: Session, Secret key Idempotent on app_user_id (an alias finds its customer). Given attributes are merged into existing ones; set a key to null to remove it. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | body | string | yes | | | `attributes` | body | object | no | Key -> value. A value may also be `{ value, updated_at_ms }` (RevenueCat format). null or "" deletes the attribute. | ### Set customer attributes `POST /v1/customers/{app_user_id}/attributes` - CLI: `salesdogg customers set-attributes ` - MCP tool: `customers_set_attributes` - Credentials: Session, Secret key, Public key Same rules as RevenueCat: string values up to 500 characters; null or "" deletes; custom keys start with a letter, use letters, digits, "_" and "-", up to 40 characters, at most 50 per customer; keys starting with "$" are reserved ($email, $displayName, $phoneNumber, $mediaSource, $campaign, $adGroup, $ad, $keyword, $creative, integration ids). Send `{ value, updated_at_ms }` to say when the change was made; an older write never replaces a newer one. Client apps may call this with the public key (creating the customer if needed), but cannot read attributes back: the response then carries entitlements only. From an app, campaign attribution keys can only be set once. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | path | string | yes | Your own identifier for the user (URL-encoded). An alias works too. | | `attributes` | body | object | yes | Key -> value. A value may also be `{ value, updated_at_ms }` (RevenueCat format). null or "" deletes the attribute. | ### Attach an anonymous customer to a known user `POST /v1/customers/identify` - CLI: `salesdogg customers identify` - MCP tool: `customers_identify` - Credentials: Session, Secret key, Public key Client SDKs start with an anonymous id (`$anon:`; RevenueCat's `$RCAnonymousID:<32 hex>` is accepted too). When the person signs in, call this to move everything the anonymous customer bought, and its attributes, to `app_user_id`. The anonymous id keeps working as an alias. Safe to repeat; if the anonymous customer does not exist it only makes sure the user exists. With a public key the response contains the user's entitlements only. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `anonymous_id` | body | string | yes | | | `app_user_id` | body | string | yes | | ### Get a customer with entitlements, subscriptions, purchases and attributes `GET /v1/customers/{app_user_id}` - CLI: `salesdogg customers get ` - MCP tool: `customers_get` - Credentials: Session, Secret key Server-side only. Client apps check access with GET /v1/customers/{app_user_id}/entitlements. An alias finds its customer. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | path | string | yes | Your own identifier for the user (URL-encoded). An alias works too. | ### Delete a customer, their aliases, attributes and purchase history `DELETE /v1/customers/{app_user_id}` - CLI: `salesdogg customers delete ` - MCP tool: `customers_delete` - Credentials: Session, Secret key (sessions need the owner or admin role) Signed-in users need the owner or admin role in the organization. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | path | string | yes | Your own identifier for the user (URL-encoded). An alias works too. | ### Check what a user has access to `GET /v1/customers/{app_user_id}/entitlements` - CLI: `salesdogg customers entitlements ` - MCP tool: `customers_entitlements` - Credentials: Session, Secret key, Public key The access check. Returns the entitlements that are active right now. Unknown users return an empty list rather than an error, so it is safe to call for any user. Calls from an app (public key, or any key with an X-Platform header) update the customer's last-seen details. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | path | string | yes | Your own identifier for the user (URL-encoded). An alias works too. | ### Grant an entitlement without a purchase `POST /v1/customers/{app_user_id}/grants` - CLI: `salesdogg customers grant ` - MCP tool: `customers_grant` - Credentials: Session, Secret key Gives the user an entitlement directly (promotions, support, testing). Creates the customer if needed. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | path | string | yes | Your own identifier for the user (URL-encoded). An alias works too. | | `entitlement` | body | string | yes | Entitlement lookup_key or id. | | `expires_at` | body | string | no | Omit for a grant that never expires. | ### Revoke granted entitlements `DELETE /v1/customers/{app_user_id}/grants/{entitlement}` - CLI: `salesdogg customers revoke-grant ` - MCP tool: `customers_revoke_grant` - Credentials: Session, Secret key Revokes all active grants of the entitlement for this user. Access from purchases is unaffected. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | path | string | yes | Your own identifier for the user (URL-encoded). An alias works too. | | `entitlement` | path | string | yes | Entitlement lookup_key or id. | ## Subscriptions ### List subscriptions `GET /v1/subscriptions` - CLI: `salesdogg subscriptions list` - MCP tool: `subscriptions_list` - Credentials: Session, Secret key | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `limit` | query | integer | no | Page size (1-100). | | `starting_after` | query | string | no | Cursor: the `next_cursor` value from the previous page. | | `app_user_id` | query | string | no | | | `status` | query | "trialing" \| "active" \| "in_grace_period" \| "past_due" \| "paused" \| "expired" | no | | | `environment` | query | "production" \| "sandbox" | no | | ### Get a subscription `GET /v1/subscriptions/{subscription_id}` - CLI: `salesdogg subscriptions get ` - MCP tool: `subscriptions_get` - Credentials: Session, Secret key | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `subscription_id` | path | string | yes | | ## Purchases ### List one-time purchases `GET /v1/purchases` - CLI: `salesdogg purchases list` - MCP tool: `purchases_list` - Credentials: Session, Secret key | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `limit` | query | integer | no | Page size (1-100). | | `starting_after` | query | string | no | Cursor: the `next_cursor` value from the previous page. | | `app_user_id` | query | string | no | | ## Receipts ### Report a purchase made in a client app `POST /v1/receipts` - CLI: `salesdogg receipts submit` - MCP tool: `receipts_submit` - Credentials: Session, Secret key, Public key Validates the purchase with the store, records it for the user and returns their updated customer record. Call it right after a purchase or restore. `token` per provider: - app_store: the StoreKit 2 transaction `jwsRepresentation`. - play_store: the purchase token (also pass `store_product_identifier` for one-time products). - stripe / braintree: the subscription id (links an existing subscription to the user). Secret key only: unlike a store receipt, the id does not prove who bought it. If the purchase already belongs to another identified user, the project's restore behavior decides (GET/PATCH /v1/settings): `transfer` moves it to this user (a `transfer` event is recorded); `keep_with_original` refuses with 409 `receipt_already_in_use`. Purchases of anonymous users always move. With a public key the response contains the user's entitlements only: `attributes`, `subscriptions` and `purchases` are empty. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | body | string | yes | | | `app_id` | body | string | no | The app (provider connection) the purchase was made through. | | `provider` | body | "stripe" \| "braintree" \| "app_store" \| "play_store" \| "simulator" | no | Alternative to app_id when the project has exactly one app for this provider. Client SDKs send this. | | `token` | body | string | yes | | | `store_product_identifier` | body | string | no | | ## Checkout ### Create a hosted checkout page for a user `POST /v1/checkout_sessions` - CLI: `salesdogg checkout create` - MCP tool: `checkout_create` - Credentials: Session, Secret key Returns a URL to send the user to. The resulting purchase is attributed to `app_user_id` automatically. Currently supported for Stripe products. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | body | string | yes | | | `product_id` | body | string | yes | Product to sell (a Stripe product). | | `success_url` | body | string | yes | | | `cancel_url` | body | string | no | | | `customer_email` | body | string | no | | | `trial_days` | body | integer | no | | ## Events ### List lifecycle events, newest first `GET /v1/events` - CLI: `salesdogg events list` - MCP tool: `events_list` - Credentials: Session, Secret key Every change to a subscription or purchase is recorded as an event, regardless of which store it came from. The same payloads are sent to webhook endpoints. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `limit` | query | integer | no | Page size (1-100). | | `starting_after` | query | string | no | Cursor: the `next_cursor` value from the previous page. | | `app_user_id` | query | string | no | | | `type` | query | "initial_purchase" \| "trial_started" \| "trial_converted" \| "renewal" \| "cancellation" \| "uncancellation" \| "billing_issue" \| "billing_recovered" \| "paused" \| "resumed" \| "product_change" \| "expiration" \| "refund" \| "non_renewing_purchase" \| "transfer" \| "grant_created" \| "grant_revoked" \| "test" | no | | ## Webhooks ### List webhook endpoints `GET /v1/webhook_endpoints` - CLI: `salesdogg webhooks list` - MCP tool: `webhooks_list` - Credentials: Session, Secret key ### Register a webhook endpoint `POST /v1/webhook_endpoints` - CLI: `salesdogg webhooks create` - MCP tool: `webhooks_create` - Credentials: Session, Secret key (sessions need the owner or admin role) Events are POSTed as JSON and retried with exponential backoff (8 attempts over about 2 days) until your endpoint returns 2xx. The signing secret is only returned here. Moving from RevenueCat: set `format: "revenuecat"` to receive RevenueCat's webhook body, and `authorization_header` to the value your receiver already checks. Events RevenueCat has no counterpart for (billing_recovered, which RevenueCat reports as the RENEWAL that follows, and grants) are not sent to such endpoints. Signed-in users need the owner or admin role in the organization. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `url` | body | string | yes | | | `event_types` | body | "initial_purchase" \| "trial_started" \| "trial_converted" \| "renewal" \| "cancellation" \| "uncancellation" \| "billing_issue" \| "billing_recovered" \| "paused" \| "resumed" \| "product_change" \| "expiration" \| "refund" \| "non_renewing_purchase" \| "transfer" \| "grant_created" \| "grant_revoked" \| "test"[] | no | Omit to receive all events. | | `format` | body | "salesdogg" \| "revenuecat" | no | | | `authorization_header` | body | string | no | Sent verbatim as the Authorization header of every delivery, as RevenueCat does. | ### Delete a webhook endpoint `DELETE /v1/webhook_endpoints/{endpoint_id}` - CLI: `salesdogg webhooks delete ` - MCP tool: `webhooks_delete` - Credentials: Session, Secret key (sessions need the owner or admin role) Signed-in users need the owner or admin role in the organization. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint_id` | path | string | yes | | ### Send a test event to all enabled endpoints `POST /v1/webhook_endpoints/{endpoint_id}/test` - CLI: `salesdogg webhooks test ` - MCP tool: `webhooks_test` - Credentials: Session, Secret key Emits an event of type "test". Check the outcome with GET /v1/webhook_deliveries. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `endpoint_id` | path | string | yes | | ### List webhook delivery attempts, newest first `GET /v1/webhook_deliveries` - CLI: `salesdogg webhooks deliveries` - MCP tool: `webhooks_deliveries` - Credentials: Session, Secret key | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `limit` | query | integer | no | Page size (1-100). | | `starting_after` | query | string | no | Cursor: the `next_cursor` value from the previous page. | | `endpoint_id` | query | string | no | | | `status` | query | "pending" \| "succeeded" \| "failed" | no | | ## Metrics ### Headline numbers for the project `GET /v1/metrics/overview` - CLI: `salesdogg metrics overview` - MCP tool: `metrics_overview` - Credentials: Session, Secret key | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `environment` | query | "production" \| "sandbox" | no | | ### Daily subscription metrics over a date range `GET /v1/metrics/timeseries` - CLI: `salesdogg metrics timeseries` - MCP tool: `metrics_timeseries` - Credentials: Session, Secret key One point per UTC day: active subscriptions, trials and MRR at the end of the day, plus the day's new subscriptions, trial conversions, churn, cancellations, refunds and revenue. At most 366 days per request. MRR is based on each subscription's current price. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `from` | query | string | no | First day (YYYY-MM-DD, UTC). Defaults to 29 days before `to`. | | `to` | query | string | no | Last day (YYYY-MM-DD, UTC). Defaults to today. | | `environment` | query | "production" \| "sandbox" | no | | ## Simulator ### Simulate a purchase `POST /v1/simulator/purchases` - CLI: `salesdogg simulator purchase` - MCP tool: `simulator_purchase` - Credentials: Session, Secret key Creates a purchase without any store, so you can build and test against SalesDogg before connecting a provider. It goes through the same path as a real purchase: entitlements update, events are recorded and your webhook endpoints are called. Simulated data is always in the sandbox environment. Name the product either way: - `store_identifier`: any name you like (e.g. "pro_monthly"). The simulator app and the product are created if they do not exist. Zero setup. - `product_id`: an existing product, including one imported from a real provider, to test your real catalog and entitlements without paying. Subscriptions start active, or trialing when `trial_days` is set. Move them on with POST /v1/simulator/subscriptions/{subscription_id}/advance. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | body | string | yes | | | `store_identifier` | body | string | no | | | `product_id` | body | string | no | | | `type` | body | "subscription" \| "one_time" | no | Only with store_identifier, when the product does not exist yet. Default subscription. | | `trial_days` | body | integer | no | Start a subscription with a free trial of this length. | | `billing_period` | body | string | no | ISO 8601 duration of one billing cycle, e.g. P1M, P1Y, P1W. | | `price_amount` | body | integer | no | Price per billing period in minor units (cents). | | `currency` | body | string | no | | ### Move a simulated subscription through its lifecycle `POST /v1/simulator/subscriptions/{subscription_id}/advance` - CLI: `salesdogg simulator advance ` - MCP tool: `simulator_advance` - Credentials: Session, Secret key Applies one change to a subscription created by the simulator and returns the events it produced: - `renew`: start the next billing period (converts a trial, recovers a billing issue, resubscribes an expired subscription). - `cancel` / `uncancel`: turn auto-renew off or back on. Access continues until `expire`. - `fail_payment` / `recover`: enter and leave the billing-issue grace period. - `pause` / `resume`. - `expire`: end the subscription now; access is removed. - `refund`: refund the last charge and end the subscription. Only simulated subscriptions can be changed this way; real ones are driven by their store. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `subscription_id` | path | string | yes | | | `action` | body | "renew" \| "cancel" \| "uncancel" \| "fail_payment" \| "recover" \| "pause" \| "resume" \| "expire" \| "refund" | yes | | ### Refund a simulated one-time purchase `POST /v1/simulator/purchases/{purchase_id}/refund` - CLI: `salesdogg simulator refund-purchase ` - MCP tool: `simulator_refund_purchase` - Credentials: Session, Secret key Marks the purchase refunded, which removes the access it granted. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `purchase_id` | path | string | yes | | ## Settings ### Get the project's settings `GET /v1/settings` - CLI: `salesdogg settings get` - MCP tool: `settings_get` - Credentials: Session, Secret key ### Change the project's settings `PATCH /v1/settings` - CLI: `salesdogg settings update` - MCP tool: `settings_update` - Credentials: Session, Secret key (sessions need the owner or admin role) Signed-in users need the owner or admin role in the organization. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `restore_behavior` | body | "transfer" \| "keep_with_original" | no | | ## RevenueCat-compatible subscribers ### Get (or create) a customer in RevenueCat's format `GET /v1/subscribers/{app_user_id}` - CLI: `salesdogg subscribers get ` - MCP tool: `subscribers_get` - Credentials: Session, Secret key RevenueCat REST API v1 compatible: same path, request and response shape. Creates the customer if it does not exist, as RevenueCat does. Send X-Platform only when the request is on behalf of the app; it updates last seen. grace_period_expires_date is always null; GET /v1/customers/{app_user_id}/entitlements is the authoritative access check. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | path | string | yes | The app user id (or any alias). | ### Delete a customer (RevenueCat format) `DELETE /v1/subscribers/{app_user_id}` - CLI: `salesdogg subscribers delete ` - MCP tool: `subscribers_delete` - Credentials: Session, Secret key (sessions need the owner or admin role) RevenueCat REST API v1 compatible: same path, request and response shape. Deletes the customer, their aliases, attributes and purchase history. Signed-in users need the owner or admin role in the organization. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | path | string | yes | The app user id (or any alias). | ### Update customer attributes in RevenueCat's format `POST /v1/subscribers/{app_user_id}/attributes` - CLI: `salesdogg subscribers set-attributes ` - MCP tool: `subscribers_set_attributes` - Credentials: Session, Secret key RevenueCat REST API v1 compatible: same path, request and response shape. Body: { "attributes": { "": { "value": "...", "updated_at_ms": 1709195668093 } } }. A null or empty value deletes the attribute; an older updated_at_ms never replaces a newer value. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | path | string | yes | The app user id (or any alias). | | `attributes` | body | object | yes | | ### Grant a promotional entitlement (RevenueCat format) `POST /v1/subscribers/{app_user_id}/entitlements/{entitlement_identifier}/promotional` - CLI: `salesdogg subscribers grant-promotional ` - MCP tool: `subscribers_grant_promotional` - Credentials: Session, Secret key RevenueCat REST API v1 compatible: same path, request and response shape. Pass duration (daily, three_day, weekly, two_week, monthly, two_month, three_month, six_month, yearly, lifetime) or end_time_ms. start_time_ms only moves the end of a duration; the grant is active from now. Same as POST /v1/customers/{app_user_id}/grants. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | path | string | yes | The app user id (or any alias). | | `entitlement_identifier` | path | string | yes | Entitlement lookup_key. | | `duration` | body | "daily" \| "three_day" \| "weekly" \| "two_week" \| "monthly" \| "two_month" \| "three_month" \| "six_month" \| "yearly" \| "lifetime" | no | | | `start_time_ms` | body | integer | no | | | `end_time_ms` | body | integer | no | | ### Revoke promotional entitlements (RevenueCat format) `POST /v1/subscribers/{app_user_id}/entitlements/{entitlement_identifier}/revoke_promotionals` - CLI: `salesdogg subscribers revoke-promotionals ` - MCP tool: `subscribers_revoke_promotionals` - Credentials: Session, Secret key RevenueCat REST API v1 compatible: same path, request and response shape. Revokes every active grant of the entitlement. Access from purchases is unaffected. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `app_user_id` | path | string | yes | The app user id (or any alias). | | `entitlement_identifier` | path | string | yes | Entitlement lookup_key. | ## Imports ### Import products, entitlements and offerings from RevenueCat `POST /v1/imports/revenuecat/catalog` - CLI: `salesdogg imports revenuecat-catalog` - MCP tool: `imports_revenuecat_catalog` - Credentials: Session, Secret key (sessions need the owner or admin role) Step 1 of moving from RevenueCat. Connect your stores first (POST /v1/apps); RevenueCat apps are matched to them by bundle id / package name, or by store when there is one app per store. Products are created under the matching app (a Stripe product becomes one product per active price), entitlements and offerings by lookup_key. Safe to run again. Signed-in users need the owner or admin role in the organization. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `api_key` | body | string | yes | A RevenueCat REST API v2 secret key with read access (project configuration and customer information). Used for this request only; never stored. | | `project_id` | body | string | yes | The RevenueCat project id (proj...). | | `app_map` | body | object | no | RevenueCat app id -> SalesDogg app id, only needed when a store has several SalesDogg apps that cannot be told apart by bundle id / package name. | ### Import one page of customers from RevenueCat `POST /v1/imports/revenuecat/customers` - CLI: `salesdogg imports revenuecat-customers` - MCP tool: `imports_revenuecat_customers` - Credentials: Session, Secret key (sessions need the owner or admin role) Step 2, after the catalog. Call repeatedly with `starting_after: next_cursor` until `has_more` is false (`salesdogg import revenuecat` does this for you). Safe to re-run any page. Brings over app user ids and aliases, attributes, first/last seen and device details, and promotional entitlements (as grants). Store purchases are looked up at the store and recorded exactly as a store notification would be: Stripe through its API, Google Play through the Orders API, the App Store when the app has App Store Server API credentials. The rest count as `awaiting_app_sync`: the SalesDogg SDK records them on the app's next launch. Expired subscriptions are skipped unless include_expired is true. Nothing imported sends webhooks. Point the stores' notifications at SalesDogg before importing so no change is missed in between. Signed-in users need the owner or admin role in the organization. | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `api_key` | body | string | yes | A RevenueCat REST API v2 secret key with read access (project configuration and customer information). Used for this request only; never stored. | | `project_id` | body | string | yes | The RevenueCat project id (proj...). | | `app_map` | body | object | no | RevenueCat app id -> SalesDogg app id, only needed when a store has several SalesDogg apps that cannot be told apart by bundle id / package name. | | `starting_after` | body | string | no | The next_cursor of the previous page. | | `limit` | body | integer | no | Customers per call. Each takes about 4 RevenueCat requests plus one store lookup per purchase. | | `include_expired` | body | boolean | no | Also look up subscriptions that have expired, for history. | ## billing ### Get the organization's SalesDogg plan, with an upgrade link on Free `GET /v1/billing` - CLI: `salesdogg billing get` - MCP tool: `billing_get` - Credentials: Session, Secret key Free is sandbox only. On Free, `upgrade_url` is a link a person can open to choose a plan and pay without signing in; show it to the user when they want to go live. Connecting live credentials on Free answers 402 `plan_required` with the same link in `error.details.upgrade_url`.