# Ad-Sharing Opt-Outs
Source: https://docs.subtotal.com/docs/ad-sharing/opt-outs
Record a consumer's request not to have their purchases shared with ad platforms.
## Overview
`POST /ad-sharing/opt-outs` records a consumer's request not to have their purchases shared with ad
platforms. The endpoint only records opt-outs; it cannot grant permission.
An opt-out takes effect after the API successfully records it and applies to sharing from that point
forward. It does not retract or delete data already delivered to ad platforms. Submit opt-outs
promptly after receiving a consumer request to meet applicable deadlines and because sharing can
continue until the API returns `204`.
Recording an opt-out cannot be undone through the API. Treat each call as permanent.
## Recording an opt-out
Identify the consumer by email or by a connection you own. Requests use the same
[API key authentication](/docs/subtotal-api) as the rest of the Subtotal API.
Submitting an email address directly:
```bash theme={null}
curl -X POST https://api.subtotal.com/ad-sharing/opt-outs \
-H "X-Api-Key: {yourkeyvalue}" \
-H "Content-Type: application/json" \
-d '{"email": "shopper@example.com"}'
```
Or naming a connection, when you have its ID and would rather not look up the address:
```bash theme={null}
curl -X POST https://api.subtotal.com/ad-sharing/opt-outs \
-H "X-Api-Key: {yourkeyvalue}" \
-H "Content-Type: application/json" \
-d '{"connection_id": "01HZY7QK8N4M2P6R3T9V5W1X0Y"}'
```
A successful call returns **`204 No Content`** with an empty body.
### Request fields
| Field | Type | Description |
| --------------- | ------ | ------------------------------------------------------------------------------------------------------ |
| `email` | string | The consumer's email address. Required unless `connection_id` is supplied. Maximum 254 characters. |
| `connection_id` | string | An owned, non-simulated connection with a usable email. When supplied, the connection's email is used. |
Supply at least one. If you supply both, `connection_id` wins and `email` is ignored.
### Responses
| Status | Meaning |
| ------ | ---------------------------------------------------------------------------------------------------------------- |
| `204` | The request was accepted. |
| `401` | The API key is missing or invalid. |
| `404` | The `connection_id` does not exist, does not belong to you, or is a simulated connection. |
| `422` | Neither field was supplied, the selected connection has no usable email, or a field exceeded its maximum length. |
Email matching ignores case and surrounding whitespace, so `Shopper@Example.com` and
`shopper@example.com` are treated as the same address.
# Create Connection
Source: https://docs.subtotal.com/docs/api-reference/connections/create-connection
post /connections
Create a new connection
# Create Connection Token
Source: https://docs.subtotal.com/docs/api-reference/connections/create-connection-token
post /connections/{connection_id}/token
Request a new connection token
# Delete Connection
Source: https://docs.subtotal.com/docs/api-reference/connections/delete-connection
delete /connections/{connection_id}
Delete a connection
# Get Connection
Source: https://docs.subtotal.com/docs/api-reference/connections/get-connection
get /connections/{connection_id}
Get a connection
# Get Connections
Source: https://docs.subtotal.com/docs/api-reference/connections/get-connections
get /connections
Get a list of connections
# Update Connection
Source: https://docs.subtotal.com/docs/api-reference/connections/update-connection
patch /connections/{connection_id}
Update a connection
# Subtotal API
Source: https://docs.subtotal.com/docs/api-reference/introduction
Build applications that integrate with Subtotal
## Overview
The Subtotal API lets brands and developers build experiences around customer-permissioned retail purchases.
The Subtotal API follows REST principles and uses standard HTTP methods and JSON payloads, making it straightforward to work with in any modern programming language or framework.
## Base URL
Append an endpoint to the base URL root address to form a complete request URL.
```
https://api.subtotal.com
```
## Authentication
All requests must include an API key for authentication.
Include the following header in each request:
```json theme={null}
headers = {
"X-Api-Key": "{yourkeyvalue}"
}
```
## Authorization
We reserve the *Authorization* header for endpoints that require a [connection token](/docs/api-reference/connections/create-connection-token).
```json theme={null}
headers = {
"Authorization": "Bearer: {connection_token}"
}
```
# Create Link Token
Source: https://docs.subtotal.com/docs/api-reference/link-tokens/create-link-token
post /link-tokens
Create a durable customer-scoped Subtotal Link token
Create tokens only from your backend with `X-Api-Key` or an OAuth access token with `connections:write`. Your client must be active.
`X-Subtotal-Idempotency-Key` is optional. Include it to return the same token when retrying an identical request. Without it, each successful request creates a new token.
Returns `link_token`, not a URL or a separate token ID. See [Customer Link tokens](/docs/subtotal-link/link-tokens) for URL construction, reuse, and retry behavior.
```bash cURL theme={null}
curl --request POST 'https://api.subtotal.com/link-tokens' \
--header "X-Api-Key: $SUBTOTAL_API_KEY" \
--header 'Content-Type: application/json' \
--data '{"customer_id":"customer-example-001","email":"consumer@example.com","expires_in_days":30}'
```
# List Link Tokens
Source: https://docs.subtotal.com/docs/api-reference/link-tokens/get-link-tokens
get /link-tokens
List link-token metadata without returning bearer secrets or consumer contact fields
```bash cURL theme={null}
curl --get 'https://api.subtotal.com/link-tokens' \
--header "X-Api-Key: $SUBTOTAL_API_KEY" \
--data-urlencode 'customer_id=customer-example-001' \
--data-urlencode 'status=active'
```
Authenticate from your backend with `X-Api-Key` or an OAuth access token with `connections:read`. An active client and the `customer_id` query parameter are required. Optionally filter by `active`, `expired`, or `revoked`.
Returns metadata only, with no raw tokens or resource IDs and no pagination. See [token recovery and lifecycle](/docs/subtotal-link/link-tokens#idempotency-and-token-recovery).
# Revoke Link Token
Source: https://docs.subtotal.com/docs/api-reference/link-tokens/revoke-link-token
delete /link-tokens/{link_token}
Permanently revoke a link token
```bash cURL theme={null}
curl --request DELETE "https://api.subtotal.com/link-tokens/$SUBTOTAL_LINK_TOKEN" \
--header "X-Api-Key: $SUBTOTAL_API_KEY"
```
Authenticate from your backend with `X-Api-Key` or an OAuth access token with `connections:write`. Use the raw token as the path parameter, and redact that request path from logs.
Returns `204` for an owned active, expired, or already-revoked token; an unknown or foreign token returns `404`. Revocation does not disconnect existing connections or invalidate already-issued connection JWTs. See [revoke or replace a URL](/docs/subtotal-link/link-tokens#revoke-or-replace-a-url).
# Get Purchase
Source: https://docs.subtotal.com/docs/api-reference/purchases/get-purchase
get /purchases/{purchase_id}
Get purchase details using partner OAuth Bearer authentication, or legacy X-API-Key plus a connection access token
# Get Purchases
Source: https://docs.subtotal.com/docs/api-reference/purchases/get-purchases
get /purchases
Get a list of purchases from a connected account
# Get Retailers
Source: https://docs.subtotal.com/docs/api-reference/retailers/get-retailers
get /retailers
Get configured retailers
## Partner app access
Partner apps can call this endpoint on behalf of the authorizing brand with an OAuth access token containing `retailers:read`. No API key is required.
```bash theme={null}
curl 'https://api.subtotal.com/retailers?include=all' \
--header "Authorization: Bearer $SUBTOTAL_ACCESS_TOKEN"
```
* `include=enabled` (default) returns retailers configured for the authorizing brand.
* `include=all` returns all active retailers. Each retailer's `enabled` and `link_url` values reflect only that brand's configuration; disabled retailers have a null `link_url`.
A token without `retailers:read` receives `403 insufficient_scope`. For older installations, follow the [reauthorization steps](/docs/partner-apps/oauth#upgrade-an-existing-installation).
Retailer `link_url` values are static URLs that are deprecated and will be removed in a future release. Use [customer Link tokens](/docs/subtotal-link/link-tokens) instead. Existing URLs continue to work for now.
# Core Concepts
Source: https://docs.subtotal.com/docs/concepts
Learn the core concepts behind how Subtotal works.
## Overview
Subtotal's platform is built around permissioned connections between shoppers, brands, and retailers.
At its core, Subtotal helps brands connect with retail shoppers and access first-party purchase data that can be used to power loyalty, marketing, analytics, and more.
This page introduces the key concepts that appear throughout the Subtotal platform and documentation.
***
## Connections
A **connection** represents the relationship between:
* a shopper
* a retailer account
* and a brand
Connections are created when a shopper successfully links a retailer account to a brand’s app or website using [Subtotal Link](/docs/subtotal-link).
***
## Purchases
A **purchase** represents a completed transaction from a connected retailer account.
Each purchase includes structured metadata describing the full details of the transaction:
* Identifiers (purchase ID, retailer, connection ID)
* Timestamps (order date, fulfillment date when available)
* Transaction totals (subtotal, tax, discounts, shipping, total)
* Items (product name, brand, upc, quantity, price, and other metadata)
Subtotal continuously syncs new purchases from connected retailer accounts and evaluates each one in real time against a consumer’s sharing preferences. When a purchase is eligible, it becomes available to brands as verified retail purchase data.
***
## Events
Subtotal turns purchases and account activity into **events**.
Events are the primary way Subtotal communicates activity to downstream systems and integrations. Examples include:
* a shopper connecting a retailer account
* a qualifying purchase being detected
* a retailer account requiring re-authentication
Events can be delivered in real time and are used to power workflows such as loyalty rewards, personalized messaging, analytics updates, and more.
***
## Integrations
An **integration** connects Subtotal to an external system, such as a marketing platform, loyalty provider, data warehouse, or internal service.
Integrations allow teams to activate purchase data without building custom infrastructure. For example, brands can use Subtotal integrations to trigger flows in email or SMS tools, update customer profiles, or sync data into analytics platforms.
***
## API and webhooks
The **Subtotal API** provides programmatic access to core platform functionality, including managing connections and accessing purchase data.
**Webhooks** allow Subtotal to push real-time events to your systems as they occur, enabling event-driven architectures and timely customer experiences.
***
## Putting it all together
At a high level, Subtotal works like this:
1. A shopper connects a retailer account to a brand using Subtotal
2. Subtotal detects purchases made through that account
3. Purchases are matched to brand products
4. Activity is emitted as real-time events
5. Brands use those events to power engagement, loyalty, and insights
These concepts appear throughout the documentation and platform. As you continue, you’ll see how they apply to specific integrations, APIs, and use cases.
# Event Simulator
Source: https://docs.subtotal.com/docs/event-simulator
Simulate real-world events such as users linking accounts and making retail purchases.
## Overview
The Event Simulator makes it easy to simulate real-world events such as users linking their
retailer accounts and making retail purchases. This allows you to test your API and webhook integrations
without linking personal accounts and making real purchases.
## Simulator Behavior
#### Supported Events
The event simulator allows you to simulate the following scenarios:
* Users linking retail accounts resulting in `connection.activated` webhooks
* Users making retail purchases resulting in `purchase.created` webhooks
* Linked accounts becoming unauthenticated resulting in `connection.unauthenticated` webhooks
* Users re-linking their accounts resuling in `connection.activated` webhooks
#### API and Webhook Interactions
You are able to retrieve details of these simulated objects via the Subtotal API, just as you would
real connections and purchases. Webhook destinations can be configured to accept simulated events
so you can safely avoid simulated events being delivered to production webhook destinations.
#### Simulated Purchases
When it comes to purchases, we ensure that all simulated purchases contain at least one product from
a brand that you are subscribed to. Additionally, simulated items are derived from real products
available at the retailer that the simulated connection was created for. This ensures realistic payloads
that contain your brand's products.
## Configure Simulated Events for Your Webhook Destination
Navigate to [*webhooks*](https://dashboard.subtotal.com/webhooks) and select the option to configure your
webhook destination
Switch the toggle on to enable simulated events for your webhook destination and select *Done*.
Ensure you have also enabled your desired event type subscriptions.
## Run Simulations
**Note**: Prior to simulating connections, you must first configure your list of retailers in
the [*Subtotal Link Configuration*](https://dashboard.subtotal.com/link)
To get started with running simulations, navigate to the [*simulator*](https://dashboard.subtotal.com/simulator).
#### Simulate an Account Link
To simulate an account link, select the option in the top right to *Simulate an Account Link*.
You will then be prompted to select a retailer for the simulated connection. After selecting the
retailer and clicking *simulate*, the simulated connection will be created and a
`connection.activated` webhook will be delivered.
#### Modify Status and Simulate Purchases
Once a simulated connection has been created, you'll see the new connection in the
list of simulated connections. The identifier displayed with the connection corresponds
to the `connection_id` for the connection, which can be used for API calls to retrieve
data related to the connection. Selecting the options for the connection displays the
ability to change the status of the connection and simulate a purchase. This results
in the `connection.[activated|unauthenticated]` and `purchase.created` events respectively.
**Note:** Simulated objects such as connections and purchases will be purged periodically.
# Link accounts in your iOS App
Source: https://docs.subtotal.com/docs/guides/ios-subtotal-link
Learn how to launch Subtotal Link in your iOS application
## Overview
We'll use [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview) to add [Subtotal Link](https://wwww.subtotal.com/docs/subtotal-link) into your iOS application.
## Create a view with a button
Add a button that to launch Subtotal Link in a WebView. The WebView handles the entire account linking process.
```swift theme={null}
struct ContentView: View {
@State private var showWebView = false
var body: some View {
NavigationStack {
Button("Link your account") {
showWebView = true
}
.navigationDestination(isPresented: $showWebView) {
SubtotalLinkView()
}
}
}
}
```
## Launch Subtotal Link in a WKWebView
Create a WKWebView to display Subtotal Link. The [WKWebViewConfiguration](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration) shown below is required for Subtotal Link to work properly.
```swift theme={null}
import SwiftUI
import WebKit
struct SubtotalLinkView: View {
var body: some View {
SubtotalWebView()
}
}
struct SubtotalWebView: UIViewRepresentable {
func makeUIView(context: Context) -> WKWebView {
let webConfiguration = WKWebViewConfiguration()
webConfiguration.allowsInlineMediaPlayback = true
webConfiguration.applicationNameForUserAgent = "Subtotal Custom WebView User Agent"
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
// Copy and paste a Link URL from the Subtotal Dashboard
let linkURL = "https://link.subtotal.com/tWGE4TJM"
if let url = URL(string: linkURL) {
webView.load(URLRequest(url: url))
}
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
// Required by UIViewRepresentable protocol - URL is already loaded in makeUIView
}
}
```
**Note:** To handle the `redirect_url` when users complete or exit linking, add a `Coordinator` with `WKNavigationDelegate` and implement `webView(_:decidePolicyFor:decisionHandler:)` to detect your URL scheme and dismiss the WebView.
## That's It!
Your iOS app now is now using Subtotal Link to link retail accounts.
# Link accounts to your Shopify store
Source: https://docs.subtotal.com/docs/guides/shopify-subtotal-link
Learn how to launch Subtotal Link from your Shopify store
## Overview
We'll create a button that customers can use to launch Subtotal Link from your Shopify store and link their accounts.
## Edit the code in your Shopify theme
Sign in to Shopify Admin. Navigate to *Online Store -> Themes -> Edit Code*.
## Create a custom button
Create a new file called `link_account_button.liquid` and customize your button using [Liquid](https://shopify.dev/docs/api/liquid).
Here's an example of a simple `Link your Walmart account` button.
Make sure you update the `href` to a [Link URL](/docs/subtotal-link/link-urls) from your Subtotal Dashboard. Include the `customer_id` query paremeter to ensure that any new connection is associated with this Shopify customer.
```liquid theme={null}
{% if customer %}
Link your Walmart Account
{% endif %}
{% schema %}
{
"name": "Subtotal Link Button",
"settings": [],
"presets": [
{
"name": "Subtotal Link Button"
}
]
}
{% endschema %}
```
## Add the button to your Shopify store
Customize your Store's theme and add the Subtotal Link Button to any page.
# Attentive
Source: https://docs.subtotal.com/docs/integrations/attentive
Send events to Attentive when a customer makes a purchase.
## Overview
Our integration with [Attentive](https://attentive.com) makes it easy to reward customers for linking accounts and making retail purchases.
We'll show you how to make the most out of this integration.
## Connect your Attentive account
Sign in to the Subtotal Dashboard and connect your Attentive account to enable the integration.
Navigate to [*Integrations -> Attentive -> Connect*](https://dashboard.subtotal.com/integrations/attentive).
Click the `Connect Attentive` button.
## Configuration
| Setting | Description |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| **Customer Identifier** | The field on the connection to use when identifying the customer in the Attentive platform: **Email**, **Customer ID**, or **Mobile**. |
## Event types
Subtotal delivers the following [custom events](https://docs.attentive.com/pages/developer-guides/custom-events/) to Attentive. Each event can be used to trigger journeys or build segments.
### Connected an Account
Sent when a customer connects a retailer account.
| Key | Type | Description |
| --------------- | ------ | ----------------------------- |
| `connection_id` | string | Identifier for the connection |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67"
}
```
### Purchased at Retailer
Sent for each historical purchase and for any newly detected purchases.
| Key | Type | Description |
| ------------- | ------ | --------------------------- |
| `purchase_id` | string | Identifier for the purchase |
**Example**
```json theme={null}
{
"purchase_id": "01KEVN1ZPM9H19JSHGN5NF0M69"
}
```
### Profile Created
Sent the first time Subtotal captures a customer's profile for a connection — their identity details and purchase metrics scoped to your brands.
| Key | Type | Description |
| -------------------------- | ------ | ------------------------------------------------------------------------------ |
| `connection_id` | string | Identifier for the connection |
| `first_name` | string | Customer's first name (`null` if unavailable) |
| `last_name` | string | Customer's last name (`null` if unavailable) |
| `email` | string | Customer's email address (`null` if unavailable) |
| `mobile` | string | Customer's mobile phone number (`null` if unavailable) |
| `postal_code` | string | Customer's postal code (`null` if unavailable) |
| `account_created_date` | string | When the customer's retailer account was created (ISO 8601; `null` if unknown) |
| `total_purchases` | number | All-time number of purchases on the connected account |
| `last_purchase_date` | string | Date of the most recent purchase (ISO 8601; `null` if none) |
| `brand_purchases` | number | Number of those purchases matching your brands |
| `last_brand_purchase_date` | string | Date of the most recent brand purchase (ISO 8601; `null` if none) |
| `brand_purchase_rate` | number | `brand_purchases` ÷ `total_purchases`, from `0.0` to `1.0` |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"first_name": "Jessica",
"last_name": "Smith",
"email": "jessica@acme.com",
"mobile": "+123456789",
"postal_code": "84101",
"account_created_date": "2024-03-12T00:00:00Z",
"total_purchases": 42,
"last_purchase_date": "2026-01-15T14:30:00Z",
"brand_purchases": 7,
"last_brand_purchase_date": "2026-01-14T09:45:00Z",
"brand_purchase_rate": 0.17
}
```
### Profile Updated
Sent when a previously captured profile changes — for example an updated email or postal code, or when new purchases shift the brand-purchase metrics. Carries the same properties as **Profile Created**.
| Key | Type | Description |
| -------------------------- | ------ | ------------------------------------------------------------------------------ |
| `connection_id` | string | Identifier for the connection |
| `first_name` | string | Customer's first name (`null` if unavailable) |
| `last_name` | string | Customer's last name (`null` if unavailable) |
| `email` | string | Customer's email address (`null` if unavailable) |
| `mobile` | string | Customer's mobile phone number (`null` if unavailable) |
| `postal_code` | string | Customer's postal code (`null` if unavailable) |
| `account_created_date` | string | When the customer's retailer account was created (ISO 8601; `null` if unknown) |
| `total_purchases` | number | All-time number of purchases on the connected account |
| `last_purchase_date` | string | Date of the most recent purchase (ISO 8601; `null` if none) |
| `brand_purchases` | number | Number of those purchases matching your brands |
| `last_brand_purchase_date` | string | Date of the most recent brand purchase (ISO 8601; `null` if none) |
| `brand_purchase_rate` | number | `brand_purchases` ÷ `total_purchases`, from `0.0` to `1.0` |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"first_name": "Jessica",
"last_name": "Smith",
"email": "jessica.smith@acme.com",
"mobile": "+123456789",
"postal_code": "84101",
"account_created_date": "2024-03-12T00:00:00Z",
"total_purchases": 43,
"last_purchase_date": "2026-02-02T18:05:00Z",
"brand_purchases": 8,
"last_brand_purchase_date": "2026-02-02T18:05:00Z",
"brand_purchase_rate": 0.19
}
```
# Event types
Source: https://docs.subtotal.com/docs/integrations/braze/event-types
Subtotal custom events and payloads delivered to Braze.
Subtotal delivers the following custom events to Braze via the [`users.track`](https://www.braze.com/docs/api/endpoints/user_data/post_user_track/) endpoint. Each event can be used to trigger campaigns, build segments, or power Canvas journeys.
## subtotal\_purchase
Triggered when a purchase is detected for a connected account. Sent for each historical purchase and for any newly detected purchases.
| Key | Type | Description |
| :------------ | :----- | :------------------------------------------------------ |
| `purchase_id` | string | Identifier for the purchase |
| `total` | string | The grand total paid by the customer |
| `tax` | string | The sales tax that was paid |
| `subtotal` | string | The subtotal of the purchase |
| `item_count` | number | The number of items in the purchase |
| `retailer` | string | Identifier for the retailer where the purchase was made |
| `upcs` | array | The UPCs associated with each item in the purchase |
| `brands` | array | The brands associated with each item in the purchase |
| `source` | string | Always `"subtotal"` |
**Example**
```json theme={null}
{
"purchase_id": "01KEVN1ZPM9H19JSHGN5NF0M69",
"total": "44.64",
"tax": "1.60",
"subtotal": "43.04",
"item_count": 15,
"retailer": "walmart",
"upcs": ["040000476528", "016000124790", "041331027878", "013000001243", "049000031171"],
"brands": ["frenchs", "heinz", "spam", "jif", "cheerios"],
"source": "subtotal"
}
```
## subtotal\_account\_connected
Triggered when a consumer links a retail account.
| Key | Type | Description |
| :-------------- | :----- | :---------------------------- |
| `connection_id` | string | Identifier for the connection |
| `retailer` | string | Identifier for the retailer |
| `source` | string | Always `"subtotal"` |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"retailer": "walmart",
"source": "subtotal"
}
```
## subtotal\_account\_disconnected
Triggered when a consumer disconnects a retail account.
| Key | Type | Description |
| :-------------- | :----- | :-------------------------------------------------------------------------------------- |
| `connection_id` | string | Identifier for the connection |
| `retailer` | string | Identifier for the retailer |
| `link_url` | string | The [Link URL](/docs/subtotal-link/link-urls) that can be used to reconnect the account |
| `source` | string | Always `"subtotal"` |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"retailer": "walmart",
"link_url": "https://link.subtotal.com/zSG5nWHy?connection_id=01KFRSK9J11G807TAY0GCYSW67",
"source": "subtotal"
}
```
## subtotal\_connection\_unauthenticated
Triggered when an active connection becomes unauthenticated and requires the customer to reauthenticate (e.g. after a retailer password change).
| Key | Type | Description |
| :-------------- | :----- | :-------------------------------------------------------------------------------------- |
| `connection_id` | string | Identifier for the connection |
| `retailer` | string | Identifier for the retailer |
| `link_url` | string | The [Link URL](/docs/subtotal-link/link-urls) that can be used to reconnect the account |
| `source` | string | Always `"subtotal"` |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"retailer": "walmart",
"link_url": "https://link.subtotal.com/zSG5nWHy?connection_id=01KFRSK9J11G807TAY0GCYSW67",
"source": "subtotal"
}
```
## subtotal\_connection\_reauthenticated
Triggered when a consumer re-authenticates a previously unauthenticated connection.
| Key | Type | Description |
| :-------------- | :----- | :---------------------------- |
| `connection_id` | string | Identifier for the connection |
| `retailer` | string | Identifier for the retailer |
| `source` | string | Always `"subtotal"` |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"retailer": "walmart",
"source": "subtotal"
}
```
## Profile Created
Triggered the first time Subtotal captures a consumer's profile for a connection — their identity details and purchase metrics scoped to your brands. Use it to enrich Braze user profiles with first-party retail identity.
| Key | Type | Description |
| :------------------------- | :----- | :----------------------------------------------------------------------------- |
| `connection_id` | string | Identifier for the connection |
| `first_name` | string | Consumer's first name (`null` if unavailable) |
| `last_name` | string | Consumer's last name (`null` if unavailable) |
| `email` | string | Consumer's email address (`null` if unavailable) |
| `mobile` | string | Consumer's mobile phone number (`null` if unavailable) |
| `postal_code` | string | Consumer's postal code (`null` if unavailable) |
| `account_created_date` | string | When the consumer's retailer account was created (ISO 8601; `null` if unknown) |
| `total_purchases` | number | All-time number of purchases on the connected account |
| `last_purchase_date` | string | Date of the most recent purchase (ISO 8601; `null` if none) |
| `brand_purchases` | number | Number of those purchases matching your brands |
| `last_brand_purchase_date` | string | Date of the most recent brand purchase (ISO 8601; `null` if none) |
| `brand_purchase_rate` | number | `brand_purchases` ÷ `total_purchases`, from `0.0` to `1.0` |
| `source` | string | Always `"subtotal"` |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"first_name": "Jessica",
"last_name": "Smith",
"email": "jessica@acme.com",
"mobile": "+123456789",
"postal_code": "84101",
"account_created_date": "2024-03-12T00:00:00Z",
"total_purchases": 42,
"last_purchase_date": "2026-01-15T14:30:00Z",
"brand_purchases": 7,
"last_brand_purchase_date": "2026-01-14T09:45:00Z",
"brand_purchase_rate": 0.17,
"source": "subtotal"
}
```
## Profile Updated
Triggered when a previously captured profile changes — for example an updated email or postal code, or when new purchases shift the brand-purchase metrics. Carries the same properties as **Profile Created**.
| Key | Type | Description |
| :------------------------- | :----- | :----------------------------------------------------------------------------- |
| `connection_id` | string | Identifier for the connection |
| `first_name` | string | Consumer's first name (`null` if unavailable) |
| `last_name` | string | Consumer's last name (`null` if unavailable) |
| `email` | string | Consumer's email address (`null` if unavailable) |
| `mobile` | string | Consumer's mobile phone number (`null` if unavailable) |
| `postal_code` | string | Consumer's postal code (`null` if unavailable) |
| `account_created_date` | string | When the consumer's retailer account was created (ISO 8601; `null` if unknown) |
| `total_purchases` | number | All-time number of purchases on the connected account |
| `last_purchase_date` | string | Date of the most recent purchase (ISO 8601; `null` if none) |
| `brand_purchases` | number | Number of those purchases matching your brands |
| `last_brand_purchase_date` | string | Date of the most recent brand purchase (ISO 8601; `null` if none) |
| `brand_purchase_rate` | number | `brand_purchases` ÷ `total_purchases`, from `0.0` to `1.0` |
| `source` | string | Always `"subtotal"` |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"first_name": "Jessica",
"last_name": "Smith",
"email": "jessica.smith@acme.com",
"mobile": "+123456789",
"postal_code": "84101",
"account_created_date": "2024-03-12T00:00:00Z",
"total_purchases": 43,
"last_purchase_date": "2026-02-02T18:05:00Z",
"brand_purchases": 8,
"last_brand_purchase_date": "2026-02-02T18:05:00Z",
"brand_purchase_rate": 0.19,
"source": "subtotal"
}
```
# Introduction
Source: https://docs.subtotal.com/docs/integrations/braze/introduction
Connect Braze and configure how customers are identified.
## Overview
Our integration with [Braze](https://braze.com) enables brands to deliver real-time retail purchase data as custom events, powering personalized messaging campaigns and customer journeys.
This integration makes it easy to:
* Trigger campaigns and Canvases based on verified retail purchases
* Engage customers when they link or unlink retail accounts
* Re-engage customers when their credentials expire with reconnection prompts
Connect your Braze account to get started.
## Prerequisites
You'll need a Braze REST API key with the **`users.track`** permission.
In your Braze dashboard, navigate to **Settings > APIs and Identifiers**. Click **Create New API Key**, give it a descriptive name (e.g. "Subtotal Integration"), and enable the **`users.track`** permission under the User Data section.
Your Braze instance determines the regional API endpoint. You can identify it from your Braze dashboard URL. For example, if your dashboard URL is `dashboard-01.braze.com`, your instance is **US-01**.
## Connect your Braze account
Sign in to the Subtotal Dashboard and connect your Braze account to enable the integration.
Go to [*Integrations > Braze > Connect*](https://dashboard.subtotal.com/integrations/braze) in the Subtotal Dashboard.
Paste the Braze REST API key you created above.
Choose the regional instance that matches your Braze dashboard (e.g. US-01, EU-01).
Select how Subtotal should match your customers to Braze user profiles.
You should now see an `Active` status on the Braze integration page.
## Configuration
After connecting, you can adjust the integration settings.
| Setting | Description |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **API Key** | Braze REST API key. Must have the `users.track` permission enabled. |
| **Braze Instance** | Regional API endpoint for your Braze account. Supported instances: US-01 through US-08, US-10, EU-01, EU-02, AU-01, ID-01, JP-01. Find this from your Braze dashboard URL. |
| **Customer Identifier Type** | How Subtotal matches customers to Braze user profiles: **Email** (email address), **Customer ID** (mapped to Braze `external_id`), or **Mobile** (phone number). |
The customer identifier type determines which field on the Subtotal connection is used to look up the corresponding Braze user profile. Make sure the identifier you choose is consistently set on your connections.
# Event types
Source: https://docs.subtotal.com/docs/integrations/klaviyo/event-types
Subtotal event types and payloads for Klaviyo segments and flows.
Each event can be used to **create segments** or **trigger flows** in Klaviyo.
Your Klaviyo event names depend on when your integration was created. Existing integrations use **Purchased at Retailer** and **Profile Created**. New integrations use **Shared a Purchase** and **Shared a Profile**. Use the names shown for your integration when configuring Klaviyo segments and flows.
## Shared a Purchase / Purchased at Retailer
Sent for each historical purchase and for any newly detected purchases. New integrations see **Shared a Purchase**; existing integrations see **Purchased at Retailer**.
| Key | Type | Description |
| :------------ | :----- | :------------------------------------------------------ |
| `purchase_id` | string | Identifier for the purchase |
| `retailer` | string | Identifier for the retailer where the purchase was made |
| `date` | string | The date of the purchase (ISO 8601) |
| `item_count` | string | The number of items in the purchase |
| `subtotal` | number | The subtotal of the purchase |
| `tax` | number | The sales tax that was paid |
| `total` | number | The grand total paid by the customer |
| `brands` | string | The brands associated with each item in the purchase |
| `upcs` | string | The UPCs associated with each item in the purchase |
Purchase events also set Klaviyo's monetary [**event value**](https://developers.klaviyo.com/en/reference/events_api_overview): the value of the items in the event — under brand-scoped access, your brand's items rather than the receipt `total`. This is Klaviyo's own value field, not a property in the table above; Klaviyo reads it for value-based segment conditions and revenue reporting.
**Example**
```json theme={null}
{
"purchase_id": "01KEVN1ZPM9H19JSHGN5NF0M69",
"retailer": "walmart",
"date": "2025-05-31T14:48:23Z",
"item_count": 15,
"subtotal": 43.04,
"tax": 1.6,
"total": 44.64,
"brands": ["frenchs", "heinz", "spam", "jif", "yogi-tea", "cheerios", "m&ms", "goya", "coca-cola"],
"upcs": ["040000476528", "016000124790", "041331027878", "041331027878", "041331027878", "041331027878", "013000001243", "049000031171", "049000040869", "049000040869", "076950450363", "037600138727", "049000067231", "041500007007", "051500255162"]
}
```
## Connected an Account
Sent when a customer connects a retailer account.
| Key | Type | Description |
| --------------- | ------ | ----------------------------- |
| `connection_id` | string | Identifier for the connection |
| `retailer` | string | Identifier for the retailer |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"retailer": "walmart"
}
```
## Disconnected an Account
Sent when a customer disconnects a retailer account.
| Key | Type | Description |
| --------------- | ------ | --------------------------------------------------------------------------------------- |
| `connection_id` | string | Identifier for the connection |
| `retailer` | string | Identifier for the retailer |
| `link_url` | string | The [Link URL](/docs/subtotal-link/link-urls) that can be used to reconnect the account |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"retailer": "walmart",
"link_url": "https://link.subtotal.com/zSG5nWHy?connection_id=01KFRSK9J11G807TAY0GCYSW67"
}
```
## Reauthenticated Connection
Sent when an unauthenticated connection becomes reauthenticated.
| Key | Type | Description |
| --------------- | ------ | ----------------------------- |
| `connection_id` | string | Identifier for the connection |
| `retailer` | string | Identifier for the retailer |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"retailer": "walmart"
}
```
## Unauthenticated Connection
Sent when an active connection becomes unauthenticated and requires the customer to reauthenticate (e.g. after a retailer password change).
| Key | Type | Description |
| --------------- | ------ | --------------------------------------------------------------------------------------- |
| `connection_id` | string | Unique identifier for the connection |
| `retailer` | string | Identifier for the retailer |
| `link_url` | string | The [Link URL](/docs/subtotal-link/link-urls) that can be used to reconnect the account |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"retailer": "walmart",
"link_url": "https://link.subtotal.com/zSG5nWHy?connection_id=01KFRSK9J11G807TAY0GCYSW67"
}
```
## Shared a Profile / Profile Created
Sent the first time Subtotal captures a customer's profile for a connection — their identity details and purchase metrics scoped to your brands. New integrations see **Shared a Profile**; existing integrations see **Profile Created**. Use it to enrich Klaviyo profiles with first-party retail identity.
| Key | Type | Description |
| -------------------------- | ------ | ------------------------------------------------------------------------------ |
| `connection_id` | string | Identifier for the connection |
| `first_name` | string | Customer's first name (`null` if unavailable) |
| `last_name` | string | Customer's last name (`null` if unavailable) |
| `email` | string | Customer's email address (`null` if unavailable) |
| `mobile` | string | Customer's mobile phone number (`null` if unavailable) |
| `postal_code` | string | Customer's postal code (`null` if unavailable) |
| `account_created_date` | string | When the customer's retailer account was created (ISO 8601; `null` if unknown) |
| `total_purchases` | number | All-time number of purchases on the connected account |
| `last_purchase_date` | string | Date of the most recent purchase (ISO 8601; `null` if none) |
| `brand_purchases` | number | Number of those purchases matching your brands |
| `last_brand_purchase_date` | string | Date of the most recent brand purchase (ISO 8601; `null` if none) |
| `brand_purchase_rate` | number | `brand_purchases` ÷ `total_purchases`, from `0.0` to `1.0` |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"first_name": "Jessica",
"last_name": "Smith",
"email": "jessica@acme.com",
"mobile": "+123456789",
"postal_code": "84101",
"account_created_date": "2024-03-12T00:00:00Z",
"total_purchases": 42,
"last_purchase_date": "2026-01-15T14:30:00Z",
"brand_purchases": 7,
"last_brand_purchase_date": "2026-01-14T09:45:00Z",
"brand_purchase_rate": 0.17
}
```
## Profile Updated
Sent when a previously captured profile changes — for example an updated email or postal code, or when new purchases shift the brand-purchase metrics. Carries the same properties as the profile-created event above.
| Key | Type | Description |
| -------------------------- | ------ | ------------------------------------------------------------------------------ |
| `connection_id` | string | Identifier for the connection |
| `first_name` | string | Customer's first name (`null` if unavailable) |
| `last_name` | string | Customer's last name (`null` if unavailable) |
| `email` | string | Customer's email address (`null` if unavailable) |
| `mobile` | string | Customer's mobile phone number (`null` if unavailable) |
| `postal_code` | string | Customer's postal code (`null` if unavailable) |
| `account_created_date` | string | When the customer's retailer account was created (ISO 8601; `null` if unknown) |
| `total_purchases` | number | All-time number of purchases on the connected account |
| `last_purchase_date` | string | Date of the most recent purchase (ISO 8601; `null` if none) |
| `brand_purchases` | number | Number of those purchases matching your brands |
| `last_brand_purchase_date` | string | Date of the most recent brand purchase (ISO 8601; `null` if none) |
| `brand_purchase_rate` | number | `brand_purchases` ÷ `total_purchases`, from `0.0` to `1.0` |
**Example**
```json theme={null}
{
"connection_id": "01KFRSK9J11G807TAY0GCYSW67",
"first_name": "Jessica",
"last_name": "Smith",
"email": "jessica.smith@acme.com",
"mobile": "+123456789",
"postal_code": "84101",
"account_created_date": "2024-03-12T00:00:00Z",
"total_purchases": 43,
"last_purchase_date": "2026-02-02T18:05:00Z",
"brand_purchases": 8,
"last_brand_purchase_date": "2026-02-02T18:05:00Z",
"brand_purchase_rate": 0.19
}
```
# Flows
Source: https://docs.subtotal.com/docs/integrations/klaviyo/flows
Trigger Klaviyo flows from Subtotal events.
Subtotal events can be used as **real-time triggers** for Klaviyo flows.
## Example: Post-purchase retail flow
1. Navigate to **Flows → Create Flow**
2. Choose **Create from scratch**
3. Set the trigger to **Purchased at Retailer** for an existing integration, or **Shared a Purchase** for a new integration
4. Build your email or SMS flow
# Introduction
Source: https://docs.subtotal.com/docs/integrations/klaviyo/introduction
Connect Klaviyo and configure how customers are identified.
## Overview
Our integration with [Klaviyo](https://klaviyo.com) enables brands to create customer segments and trigger automated flows using verified retail purchases and account connection events from Subtotal.
This integration makes it easy to:
* Segment customers based on real-world retail purchases
* Trigger flows when customers link or unlink accounts
* Engage with retail customers in real-time
Connect your Klaviyo account to get started.
## Connect your Klaviyo account
Sign in to the Subtotal Dashboard and connect your Klaviyo account to enable the integration.
Navigate to [*Integrations → Klaviyo → Connect*](https://dashboard.subtotal.com/integrations/klaviyo)
Click the **Connect Klaviyo** button.
Review permissions then click **Allow**.
You should now see an `Active` status on the Klaviyo integration page.
## Configuration
After connecting, you can adjust how Subtotal matches your customers in Klaviyo.
| Setting | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Customer Identifier** | The field on the connection to use when identifying the customer in the Klaviyo platform: **Email**, **Customer ID**, or **Mobile**. |
# Customer segments
Source: https://docs.subtotal.com/docs/integrations/klaviyo/segments
Create Klaviyo segments from Subtotal events.
Subtotal events can be used directly in Klaviyo segments.
## Example: Active retail customers
1. In Klaviyo, navigate to **Audience → Segments**
2. Click **Create Segment**
3. Choose **What someone has done**
4. Select **Purchased at Retailer** for an existing integration, or **Shared a Purchase** for a new integration
5. Optional: Filter on **retailer** or **UPCs**
# Introduction
Source: https://docs.subtotal.com/docs/integrations/okendo/introduction
Connect Okendo and configure how customers are rewarded.
## Overview
Our integration with [Okendo](https://www.okendo.io) makes it easy to reward customers for linking accounts and making retail purchases.
We'll show you how to set up your program with Okendo and make the most out of this integration.
## Get your Okendo credentials
Sign in to your Okendo admin and navigate to *Settings -> Integrations -> Credentials*. You'll need two values:
* **User ID** — the unique user ID for your store
* **Merchant REST API Key** — click `Generate` to create one
## Connect your Okendo account
Sign in to the Subtotal Dashboard and connect your Okendo account to enable the integration.
Navigate to [*Integrations -> Okendo -> Connect*](https://dashboard.subtotal.com/integrations/okendo).
Click the `Connect Okendo` button.
Provide your Okendo User ID and Merchant REST API Key, then click `Save Integration`.
You should now see an `Active` status on the Okendo integration page.
## Configure actions
After connecting, configure the actions Subtotal should send to Okendo. Each action maps a Subtotal event (e.g. an account being linked, a purchase being made) to a custom earning rule you've set up in Okendo.
| Setting | Description |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Customer identifier** | The field on the connection used to identify the customer in Okendo. The Subtotal Okendo integration currently only supports identifying customers by **Email**. |
| **Actions** | One or more event-to-earning-rule mappings. Each action specifies the event type, retailers it applies to, and the Okendo earning rule ID. |
To add an action, click `Add action` and choose an event:
Each action requires:
| Field | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Event** | `account.linked` (fired when a customer links a retailer account) or `purchase.created` (fired for each purchase Subtotal collects). |
| **Action name** | A human-readable label for this action. |
| **Retailers** | Restrict the action to specific retailers, or apply it to all. |
| **Okendo earning rule ID** | The `Action ID` of the custom earning rule you configured in Okendo. |
See [Rewarding linked accounts](/docs/integrations/okendo/linked-account) and [Rewarding purchases](/docs/integrations/okendo/purchases) for how to set up the matching custom earning rules in Okendo.
## Customer identification
The Okendo integration identifies customers by their **email address**. How the email is captured depends on your setup:
* **Shopify app** — the customer's email is captured automatically from their Shopify profile. No additional parameters needed.
* **API integration** — include the `email` field when creating a connection via [`POST /connections`](/docs/api-reference/connections/create-connection). Without it, Subtotal won't be able to identify the customer in Okendo.
# Rewarding linked accounts
Source: https://docs.subtotal.com/docs/integrations/okendo/linked-account
Set up Okendo to reward points when customers link a retailer account.
## Create a custom earning rule in Okendo
In your Okendo admin, navigate to the Loyalty Program and create a new custom earning rule for linked accounts.
Set the **Action Type** to **Award a fixed amount of points** and choose the number of points you'd like to award each time a customer links an account.
Save the rule, then copy its `Action ID` — you'll need it in the next step.
Customers can earn from this action one time per retailer. Linking and unlinking the same account repeatedly will not result in additional points.
## Add the action in Subtotal
In the Subtotal Dashboard, navigate to your [Okendo integration](https://dashboard.subtotal.com/integrations/okendo) and click `Add action`.
Choose `account.linked` as the event, give the action a name, optionally restrict to specific retailers, and paste the Okendo earning rule's `Action ID` into the **Okendo earning rule ID** field.
Click `Create`. Subtotal will now trigger this earning rule whenever a customer links a retailer account.
# Rewarding purchases
Source: https://docs.subtotal.com/docs/integrations/okendo/purchases
Set up Okendo to reward points when customers make retail purchases.
## Create a custom earning rule in Okendo
In your Okendo admin, navigate to the Loyalty Program and create a new custom earning rule for purchases.
Set the **Action Type** to **Award a variable amount of points** and configure the **Points Earned** rate (e.g. `1` point per `1` dollar). Subtotal will pass the purchase total as the `value`, and Okendo will multiply by your configured rate to determine points awarded.
Save the rule, then copy its `Action ID` — you'll need it in the next step.
Points are calculated by Okendo using the **Points Earned** rate configured on this rule. Subtotal sends the raw dollar amount of the purchase; the conversion to points happens on Okendo's side.
## Add the action in Subtotal
In the Subtotal Dashboard, navigate to your [Okendo integration](https://dashboard.subtotal.com/integrations/okendo) and click `Add action`.
Choose `purchase.created` as the event, give the action a name, optionally restrict to specific retailers, paste the Okendo earning rule's `Action ID` into the **Okendo earning rule ID** field, and set a **Start date**. Subtotal will only reward purchases made on or after the start date — this controls how much historical purchase data gets sent to Okendo when a customer first links their account.
Click `Create`. Subtotal will now trigger this earning rule for each purchase it collects on a linked account.
# Introduction
Source: https://docs.subtotal.com/docs/integrations/rivo/introduction
Connect Rivo and configure how customers are rewarded.
## Overview
Our integration with [Rivo](https://rivo.io) makes it easy to reward customers for linking accounts and making retail purchases.
Subtotal awards points in Rivo by posting events to Rivo's Custom Action API. Each action you configure in Subtotal maps to a matching Custom Action you've created in Rivo — so Rivo can apply its earning rule and display the reward in the customer's activity feed.
## Get your Rivo API key
Sign in to the Rivo app in your Shopify admin and navigate to *Settings → Developer Toolkit → API Keys*. Copy the REST API key — you'll need it to connect the integration.
Can't find your API key? [Rivo's help article has a walkthrough.](https://help.rivo.io/en/articles/8544831-how-to-get-your-rivo-api-key)
## Connect your Rivo account
Sign in to the Subtotal Dashboard and connect your Rivo account to enable the integration.
Navigate to [*Integrations → Rivo → Connect*](https://dashboard.subtotal.com/integrations/rivo).
Click the `Connect Rivo` button.
Paste your Rivo API key, then click `Save Integration`.
You should now see an `Active` status on the Rivo integration page.
## Configure actions
After connecting, configure the actions Subtotal should send to Rivo. Each action maps a Subtotal event (an account being linked, a purchase being made) to a **Custom Action** you've set up in Rivo. The **Action name** in Subtotal must match the Custom Action name in Rivo exactly — that's how Rivo applies the matching earning rule.
| Setting | Description |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **Customer identifier** | The field on the connection used to identify the customer in Rivo: **Email**, **Customer ID**, or **Mobile**. |
| **Actions** | One or more event-to-Custom-Action mappings. Each action specifies the event type, the retailers it applies to, and how points are determined. |
To add an action, click `Add action` and choose an event:
| Field | Description |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Event** | `account.linked` (fired when a customer links a retailer account) or `purchase.created` (fired for each purchase Subtotal collects). |
| **Action name** | Must match the name of a Custom Action you've created in Rivo. Subtotal sends this value in the `custom_action_name` field so Rivo can apply the right earning rule. |
| **Retailers** | Restrict the action to specific retailers, or apply it to all. |
| **Points** *(account.linked)* | Number of points to award when a customer links an account. Subtotal sends this value to Rivo as `points_amount` on every event, so the points configured on your Rivo Custom Action rule are always overridden. |
| **Points per dollar** *(purchase.created)* | Points awarded per dollar of spend on items Subtotal collects from the purchase. Subtotal computes points as `ceil(items_subtotal × points_per_dollar)` — where `items_subtotal` is the sum of `price × quantity` for each collected item — and sends the resulting integer to Rivo. Taxes, shipping, and fees are excluded. |
| **Start date** *(purchase.created)* | Only purchases made on or after this date are rewarded. This controls how much historical purchase data gets sent to Rivo when a customer first links their account. |
See [Rewarding linked accounts](/docs/integrations/rivo/linked-account) and [Rewarding purchases](/docs/integrations/rivo/purchases) for setting up each action end-to-end in Rivo and Subtotal.
## Customer identification
The value Subtotal sends in the `customer_identifier` field depends on the **Customer identifier** type you've configured:
| Identifier type | Field on the connection |
| --------------- | ----------------------- |
| **Email** | `email` |
| **Customer ID** | `customer_id` |
| **Mobile** | `phone_number` |
How this field gets captured depends on your setup:
* **Shopify app** — the customer's email is captured automatically from their Shopify profile. For `Customer ID` or `Mobile`, you'll need to pass the value explicitly when launching Subtotal Link.
* **API integration** — include the appropriate field (`email`, `customer_id`, or `phone_number`) when creating a connection via [`POST /connections`](/docs/api-reference/connections/create-connection).
* **Subtotal Link query parameter** — pass `customer_id` (or the appropriate field) as a query parameter when launching Subtotal Link. See [Link URLs](/docs/subtotal-link/link-urls) and [Link accounts to your Shopify store](/docs/guides/shopify-subtotal-link).
Without the configured identifier field set on the connection, Subtotal won't be able to identify the customer in Rivo and the action will be skipped.
# Rewarding linked accounts
Source: https://docs.subtotal.com/docs/integrations/rivo/linked-account
Set up Rivo to reward points when customers link a retailer account.
## Create a Custom Action in Rivo
In your Rivo admin, navigate to the Loyalty Program and click `Add Another Way to Earn`. Choose `Custom Action`.
Give the action a name (e.g. `Link an Account`). The points you set on the rule will be overridden by Subtotal, so you can leave them at a placeholder value (e.g. `1`) — the actual reward comes from the Subtotal-side configuration you'll set up next. Save the rule.
Keep the **Action Name** value handy — you'll use the exact same string in the matching Subtotal action.
## Add the action in Subtotal
In the Subtotal Dashboard, navigate to your [Rivo integration](https://dashboard.subtotal.com/integrations/rivo) and click `Add action`.
Choose `account.linked` as the event, and set **Action name** to the same value you used on your Rivo Custom Action. Optionally restrict to specific retailers.
Set the **Points** field to the number of points you want to award. Subtotal sends this value to Rivo as `points_amount` on every `account.linked` event, so whatever you set here is what the customer receives.
Click `Create`. Subtotal will now trigger the matching Custom Action in Rivo whenever a customer links a qualifying retailer account.
Because Subtotal always supplies `points_amount`, the points configured on your Rivo Custom Action rule are effectively overridden. You can leave the rule's points at a placeholder value — the actual reward comes from the Subtotal **Points** field.
Points are awarded on the first successful activation of each connection. Re-authenticating an existing connection (for example, after a retailer password change) does not re-award points. Creating a fresh connection after a disconnect will.
# Rewarding purchases
Source: https://docs.subtotal.com/docs/integrations/rivo/purchases
Set up Rivo to reward points when customers make retail purchases.
## Create a Custom Action in Rivo
In your Rivo admin, navigate to the Loyalty Program and click `Add Another Way to Earn`. Choose `Custom Action`.
Give the action a name (e.g. `Make a Retail Purchase`). Save the rule.
Keep the **Action Name** value handy — you'll use the exact same string in the matching Subtotal action.
Subtotal pre-computes the points for each purchase and sends the value to Rivo, so the points configured on this Rivo rule are always overridden. You can leave the rule's points at a placeholder value (e.g. `1`) — the actual reward is calculated in Subtotal.
## Add the action in Subtotal
In the Subtotal Dashboard, navigate to your [Rivo integration](https://dashboard.subtotal.com/integrations/rivo) and click `Add action`.
Choose `purchase.created` as the event, and set **Action name** to the same value you used on your Rivo Custom Action. Optionally restrict to specific retailers, set **Points per dollar**, and set a **Start date**. Subtotal will only reward purchases made on or after the start date — this controls how much historical purchase data gets sent to Rivo when a customer first links their account.
Click `Create`. Subtotal will now trigger the matching Custom Action in Rivo for each qualifying purchase it collects.
Subtotal computes the point value for each purchase as `ceil(items_subtotal × points_per_dollar)` — where `items_subtotal` is the sum of `price × quantity` for each item Subtotal collects from the receipt — and sends the resulting integer to Rivo. Taxes, shipping, and other fees are not included. One Rivo points event is posted per qualifying purchase.
# Introduction
Source: https://docs.subtotal.com/docs/integrations/smile/introduction
Connect Smile and configure how customers are rewarded.
## Overview
Our integration with [Smile](https://smile.io) makes it easy to reward customers for linking accounts and making retail purchases.
Subtotal awards points directly in Smile by posting points transactions against each customer's Smile profile. No matching earning rule needs to exist on the Smile side — Subtotal calculates the points, then records the transaction with a description you choose.
## Connect your Smile account
Sign in to the Subtotal Dashboard and navigate to [*Integrations → Smile*](https://dashboard.subtotal.com/integrations/smile).
Click `Connect Smile`.
You'll be taken to Smile to approve the connection. Review the access Subtotal is requesting and click through to authorize it.
Smile returns you to the Subtotal Dashboard, and the integration shows an `Active` status.
No API key is required, and there is nothing to configure in Smile beforehand — approving the connection is the only setup on the Smile side. Subtotal holds a token that it refreshes automatically; you never need to rotate a credential by hand.
## How Subtotal awards points in Smile
For each qualifying event, Subtotal:
1. Looks up the Smile customer by **email** (`GET /v1/customers?email=…`).
2. POSTs a points transaction (`POST /v1/points_transactions`) with the computed point value and the **Action name** you set as the transaction description.
The description shows up in the customer's Smile activity feed, so choose something customer-friendly (e.g. `Retail purchase`, `Linked a retail account`). Subtotal does not reference any Smile-side earning rule or custom activity token — approving the connection is the only setup required in Smile.
## Configure actions
After connecting, configure the actions Subtotal should send to Smile. Each action maps a Subtotal event (an account being linked, a purchase being made) to a points award.
| Setting | Description |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Customer identifier** | The field on the connection used to identify the customer in Smile. The Subtotal Smile integration currently only supports identifying customers by **Email**. |
| **Actions** | One or more event-to-reward mappings. Each action specifies the event type, retailers it applies to, and the points to award. |
To add an action, click `Add action` and choose an event:
| Field | Description |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Event** | `account.linked` (fired when a customer links a retailer account) or `purchase.created` (fired for each purchase Subtotal collects). |
| **Action name** | A human-readable label. Sent to Smile as the points transaction description — this is what the customer will see in their Smile activity feed. |
| **Retailers** | Restrict the action to specific retailers, or apply it to all. |
| **Points** *(account.linked)* | Flat number of points awarded when a customer links a qualifying account. |
| **Points per dollar** *(purchase.created)* | Points awarded per dollar of spend on items Subtotal collects from the purchase. Subtotal computes points as `ceil(items_subtotal × points_per_dollar)` — where `items_subtotal` is the sum of `price × quantity` for each collected item — and sends the resulting integer to Smile. Taxes, shipping, and other fees are excluded. |
| **Start date** *(purchase.created)* | Only purchases made on or after this date are rewarded. This controls how much historical purchase data gets sent to Smile when a customer first links their account. |
See [Rewarding linked accounts](/docs/integrations/smile/linked-account) and [Rewarding purchases](/docs/integrations/smile/purchases) for examples of each action type.
## Customer identification
The Smile integration identifies customers by their **email address**. Subtotal calls Smile's `GET /v1/customers?email=…` endpoint to resolve the Smile customer for each connection, then awards points against that customer's profile.
How the email is captured depends on your setup:
* **Shopify app** — the customer's email is captured automatically from their Shopify profile. No additional parameters needed.
* **API integration** — include the `email` field when creating a connection via [`POST /connections`](/docs/api-reference/connections/create-connection). Without it, Subtotal won't be able to identify the customer in Smile.
If no Smile customer exists for a connection's email (e.g. the customer hasn't been created in Smile yet), the action is skipped and logged. The customer must exist in Smile at the time the event is processed — points cannot be awarded retroactively.
# Rewarding linked accounts
Source: https://docs.subtotal.com/docs/integrations/smile/linked-account
Award Smile points when a customer links a retailer account.
In the Subtotal Dashboard, navigate to your [Smile integration](https://dashboard.subtotal.com/integrations/smile) and click `Add action`.
Choose `account.linked` as the event, give the action a name, optionally restrict to specific retailers, and set the **Points** value. The action name is what the customer will see in their Smile activity feed, so pick something recognizable (e.g. `Linked a retail account`).
Click `Create`. Subtotal will now award the configured points in Smile whenever a customer links a qualifying retailer account.
Points are awarded on the first successful activation of each connection. Re-authenticating an existing connection (for example, after a retailer password change) does not re-award points. Creating a fresh connection after a disconnect will.
# Rewarding purchases
Source: https://docs.subtotal.com/docs/integrations/smile/purchases
Award Smile points for each retail purchase Subtotal collects.
In the Subtotal Dashboard, navigate to your [Smile integration](https://dashboard.subtotal.com/integrations/smile) and click `Add action`.
Choose `purchase.created` as the event, give the action a name, optionally restrict to specific retailers, set **Points per dollar**, and set a **Start date**. Subtotal will only reward purchases made on or after the start date — this controls how much historical purchase data gets sent to Smile when a customer first links their account.
Click `Create`. Subtotal will now award points in Smile for each qualifying purchase it collects, using the action name you set as the transaction description in the customer's Smile activity feed.
Subtotal computes the point value for each purchase as `ceil(items_subtotal × points_per_dollar)` — where `items_subtotal` is the sum of `price × quantity` for each item Subtotal collects from the receipt — and posts the resulting integer to Smile's `/v1/points_transactions` endpoint. Taxes, shipping, and other fees are not included. One points transaction is posted per qualifying purchase.
# Introduction
Source: https://docs.subtotal.com/docs/integrations/yotpo/introduction
Connect Yotpo and configure how customers are rewarded.
## Overview
Our integration with [Yotpo](https://yotpo.com) makes it easy to reward customers for linking accounts and making retail purchases.
Subtotal awards points in Yotpo by posting events to Yotpo's Custom Action API. Each action you configure in Subtotal maps to a matching Custom Action you've created in Yotpo — so Yotpo can apply its earning rule and display the reward in the customer's activity feed.
## Connect your Yotpo account
Sign in to the Subtotal Dashboard and connect your Yotpo account to enable the integration.
Navigate to [*Integrations → Yotpo → Connect*](https://dashboard.subtotal.com/integrations/yotpo).
Click the `Connect Yotpo` button.
Review the access permissions Subtotal requests, then click `Connect` to complete the OAuth handshake.
You should now see an `Active` status on the Yotpo integration page.
## Configure actions
After connecting, configure the actions Subtotal should send to Yotpo. Each action maps a Subtotal event (an account being linked, a purchase being made) to a **Custom Action** you've set up in Yotpo. The **Action name** in Subtotal must match the Custom Action name in Yotpo exactly — that's how Yotpo applies the matching earning rule.
| Setting | Description |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **Customer identifier** | The field on the connection used to identify the customer in Yotpo: **Email**, **Customer ID**, or **Mobile**. |
| **Actions** | One or more event-to-Custom-Action mappings. Each action specifies the event type, the retailers it applies to, and how points are determined. |
To add an action, click `Add action` and choose an event:
| Field | Description |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Event** | `account.linked` (fired when a customer links a retailer account) or `purchase.created` (fired for each purchase Subtotal collects). |
| **Action name** | Must match the name of a Custom Action you've created in Yotpo. Subtotal sends this value in the `action_name` field so Yotpo can apply the right earning rule. |
| **Retailers** | Restrict the action to specific retailers, or apply it to all. |
| **Points** *(account.linked)* | Not used for Yotpo. Yotpo always uses the points configured on your Custom Action rule for `account.linked` events — this field has no effect. |
| **Points per dollar** *(purchase.created)* | Points awarded per dollar of spend on items Subtotal collects from the purchase. Subtotal computes points as `ceil(items_subtotal × points_per_dollar)` — where `items_subtotal` is the sum of `price × quantity` for each collected item — and sends the resulting integer to Yotpo. Taxes, shipping, and fees are excluded. |
| **Start date** *(purchase.created)* | Only purchases made on or after this date are rewarded. This controls how much historical purchase data gets sent to Yotpo when a customer first links their account. |
See [Rewarding linked accounts](/docs/integrations/yotpo/linked-account) and [Rewarding purchases](/docs/integrations/yotpo/purchases) for setting up each action end-to-end in Yotpo and Subtotal.
## Customer identification
When Subtotal posts an event to Yotpo, it always includes the customer's **email** (from the connection) plus the identifier value matching your configured type:
| Identifier type | Value sent to Yotpo as `customer_id` |
| --------------- | ------------------------------------ |
| **Email** | `email` |
| **Customer ID** | `customer_id` |
| **Mobile** | `phone_number` |
Because email is always sent alongside the configured identifier, the connection's `email` field should be populated regardless of which identifier type you choose. How the fields are captured depends on your setup:
* **Shopify app** — the customer's email is captured automatically from their Shopify profile. For `Customer ID` or `Mobile`, pass the value explicitly when launching Subtotal Link.
* **API integration** — include `email` and (if applicable) `customer_id` or `phone_number` when creating a connection via [`POST /connections`](/docs/api-reference/connections/create-connection).
* **Subtotal Link query parameter** — pass `customer_id` (or the appropriate field) as a query parameter when launching Subtotal Link. See [Link URLs](/docs/subtotal-link/link-urls) and [Link accounts to your Shopify store](/docs/guides/shopify-subtotal-link).
Yotpo matches customers on both email and the configured identifier. If the connection doesn't have the email set, Yotpo may be unable to associate the event with a customer profile.
# Rewarding linked accounts
Source: https://docs.subtotal.com/docs/integrations/yotpo/linked-account
Set up Yotpo to reward points when customers link a retailer account.
## Create a Custom Action in Yotpo
In your Yotpo admin, navigate to [*Manage Program → Rewards Program → Manage earning rules*](https://loyalty-app.yotpo.com/rewards-program/earning-rules) and click `Create earning rule`. Choose `Custom Action`.
Set the **Action name** (e.g. `Linked an Account`) and configure the **Reward type** as a fixed number of points. Save the rule.
Keep the **Action name** value handy — you'll use the exact same string in the matching Subtotal action.
## Add the action in Subtotal
In the Subtotal Dashboard, navigate to your [Yotpo integration](https://dashboard.subtotal.com/integrations/yotpo) and click `Add action`.
Choose `account.linked` as the event, and set **Action name** to the same value you used on your Yotpo Custom Action. Optionally restrict to specific retailers.
Subtotal does not send a point value for Yotpo `account.linked` events — the points are taken from the rule you configured on the Yotpo Custom Action. The **Points** field in the Subtotal form is not used for Yotpo and can be left at `0`.
Click `Create`. Subtotal will now trigger the matching Custom Action in Yotpo whenever a customer links a qualifying retailer account.
Points are awarded on the first successful activation of each connection. Re-authenticating an existing connection (for example, after a retailer password change) does not re-award points. Creating a fresh connection after a disconnect will.
# Rewarding purchases
Source: https://docs.subtotal.com/docs/integrations/yotpo/purchases
Set up Yotpo to reward points when customers make retail purchases.
## Create a Custom Action in Yotpo
In your Yotpo admin, navigate to [*Manage Program → Rewards Program → Manage earning rules*](https://loyalty-app.yotpo.com/rewards-program/earning-rules) and click `Create earning rule`. Choose `Custom Action`.
Set the **Action name** (e.g. `Made a Purchase`). Save the rule.
Keep the **Action name** value handy — you'll use the exact same string in the matching Subtotal action.
Subtotal pre-computes the points for each purchase and sends the value to Yotpo, so the points configured on this Yotpo rule are always overridden. You can leave the rule's points at a placeholder value (e.g. `1`) — the actual reward is calculated in Subtotal.
## Add the action in Subtotal
In the Subtotal Dashboard, navigate to your [Yotpo integration](https://dashboard.subtotal.com/integrations/yotpo) and click `Add action`.
Choose `purchase.created` as the event, and set **Action name** to the same value you used on your Yotpo Custom Action. Optionally restrict to specific retailers, set **Points per dollar**, and set a **Start date**. Subtotal will only reward purchases made on or after the start date — this controls how much historical purchase data gets sent to Yotpo when a customer first links their account.
Click `Create`. Subtotal will now trigger the matching Custom Action in Yotpo for each qualifying purchase it collects.
Subtotal computes the point value for each purchase as `ceil(items_subtotal × points_per_dollar)` — where `items_subtotal` is the sum of `price × quantity` for each item Subtotal collects from the receipt — and sends the resulting integer to Yotpo as `reward_points`. Taxes, shipping, and other fees are not included. One Yotpo custom action event is posted per qualifying purchase.
# Welcome to the Docs
Source: https://docs.subtotal.com/docs/introduction
Learn how to make the most out of the Subtotal platform.
## Overview
Subtotal is an open commerce platform that helps brands, shoppers, and retailers build stronger relationships.
Shoppers use Subtotal to securely connect their retailer accounts—like Walmart, Amazon, and Sephora—to brand websites and apps, sharing first-party purchase data with the brands they engage with.
Subtotal turns those purchases into real-time events that power engagement, retention, and insights—enabling brands to reward customers, personalize experiences, and turn previously anonymous retail shoppers into known customers.
This documentation is a shared knowledge center for anyone building, integrating, or operating with Subtotal—including developers, marketers, operators, and partners.
## Getting started
Use Subtotal to connect retail purchases to your products, marketing, and customer experiences.
Here are a few steps to help you get started.
Sign up for Subtotal and link your first account in minutes.
Add your brand name, logo and configure a list of retailers.
Build with Subtotal by integrating with the Subtotal API.
Connect Subtotal with your existing tools and platforms.
Give AI agents read-only access to your purchase data via MCP.
## Support
We are here to help. If you need assistance, have questions, or encounter any issues, please reach out to our support team.
```
support@subtotal.com
```
# Add Subtotal to Claude
Source: https://docs.subtotal.com/docs/mcp-server/claude-connector
Connect Subtotal as a Claude connector so you can query your purchase data directly in conversations.
## Overview
Subtotal is available as a connector in Claude. Once connected, you can ask Claude questions about your purchase data — connections, purchases, items, retailers, products, and brands — directly in any conversation. Authentication is handled through OAuth, so there are no API keys to manage.
## For Team & Enterprise admins
Organization owners add Subtotal to make it available for all members. Each member authenticates with their own Subtotal account.
In Claude, go to **Organization Settings > Connectors** and click **Add**.
Select **Custom**, then **Web**. Enter the following server URL:
```
https://mcp.subtotal.com/mcp
```
Click **Add**.
Subtotal is now available to all members of your organization. Each member will need to connect their own Subtotal account (see below).
## For individual users
Pro and Max users can add Subtotal directly without an organization admin.
Go to **Settings > Connectors** and click **Add custom connector**.
Enter the following URL and click **Add**:
```
https://mcp.subtotal.com/mcp
```
## Connect your account
Whether added by an admin or by you directly, you need to connect your Subtotal account before using the connector.
Go to **Settings > Connectors** and find Subtotal. Click **Connect**.
An authorization window will open. Sign in with your Subtotal Dashboard credentials.
If you belong to multiple teams, select which team's data you want to connect.
Review the permissions and click **Approve**. The window will close and your connector status will update to connected.
The data you can access is determined by your team's settings in the [Subtotal Dashboard](https://dashboard.subtotal.com). To change what data is available, update your team's access scope in the dashboard.
Your connector is tied to the team you select during authorization. To switch teams, disconnect and reconnect — you'll be prompted to choose a team again.
## Use in conversations
Once connected, enable Subtotal in any conversation by clicking the **+** button and toggling the Subtotal connector on. Then ask questions naturally — Claude will use the Subtotal tools automatically.
See [Tools](/docs/mcp-server/tools) for a full reference and [Example Questions](/docs/mcp-server/example-queries) for common queries to try.
## Disconnect
To disconnect Subtotal, go to **Settings > Connectors**, find Subtotal, and click **Disconnect**. This revokes the OAuth token — no data will be accessible until you reconnect.
# Example Questions
Source: https://docs.subtotal.com/docs/mcp-server/example-queries
Try common questions with the Subtotal Data MCP.
These examples show common questions you can pass to the `ask` tool. Try them as-is or adapt them to your data.
### Connections
* "How many connections do I have by status?"
* "Show me all active connections created in the last 30 days"
* "Which retailers have the most connections?"
### Purchases
* "What are my 25 most recent purchases with totals?"
* "Show me monthly purchase trends for the last 12 months"
* "What's the total spend by retailer?"
* "How many purchases came in this week?"
### Items & brands
* "What are the top 20 brands across my items?"
* "Search for items containing 'protein' in the last 90 days"
* "What's the average item price by retailer?"
* "Show me items with UPC 012345678901"
### Aggregations
* "What's my total spend across all retailers this year?"
* "How many unique connections have made purchases in the last 30 days?"
* "What's the average order value by month?"
All queries are automatically scoped to your account — you can only access your own data. No additional filtering is needed.
# Subtotal Data MCP
Source: https://docs.subtotal.com/docs/mcp-server/introduction
Give AI agents read-only access to your purchase data via the Model Context Protocol.
## Overview
The Subtotal Data MCP gives AI agents read-only access to your purchase data through the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP). Connect any MCP-compatible client to query connections, purchases, items, retailers, products, and brands using natural language.
**Use cases:**
* AI shopping assistants that reference real purchase history
* Analytics copilots that answer questions about customer behavior
* Purchase data Q\&A for support and operations teams
## MCP Server URL
All requests use this URL:
```
https://mcp.subtotal.com/mcp
```
## Authentication
The Subtotal Data MCP uses OAuth. Add Subtotal as a [Claude connector](/docs/mcp-server/claude-connector) and authentication is handled automatically — no API keys to manage.
## Security and limits
All queries are scoped to your team — you can only access your own team's data. The following limits apply:
| Constraint | Limit |
| :----------------- | :----------------------------------------------------------------------------------------- |
| Rate limit | 60 queries per minute |
| Row limit | 100 results per request |
| Response size | 5 MB max |
| Query timeout | 5 seconds |
| Allowed operations | SELECT only |
| Input format | Natural language for `ask` (SQL is rejected); `get_retailers` takes an `include` parameter |
For a full reference of available tools, see the [Tools](/docs/mcp-server/tools) page.
# Tools
Source: https://docs.subtotal.com/docs/mcp-server/tools
Reference for all tools available in the Subtotal Data MCP.
The Subtotal Data MCP provides three tools for exploring and querying your purchase data.
## ask
Ask a natural language question about your purchase data. The server translates it into SQL and returns the results — no SQL knowledge required.
**Parameters:**
| Name | Type | Required | Description |
| :--------- | :----- | :------- | :--------------------------------------------------- |
| `question` | string | Yes | A natural language question about your purchase data |
**Example questions:**
* "Show me the top 5 retailers by purchase count this year"
* "How many active connections do I have?"
* "What are my highest-value purchases from the last 30 days?"
* "Which brands appear most frequently in my items?"
**Response format:**
```json theme={null}
{
"fields": ["name", "purchase_count"],
"results": [
["Amazon", 1523],
["Target", 892],
["Walmart", 641]
],
"result_count": 3
}
```
**Constraints:**
* Questions must be plain English — embedded SQL will be rejected
* Only `SELECT` queries are generated — your data is never modified
* Results are limited to 100 rows and 5 MB
* A 5-second execution timeout is enforced
If the question cannot be answered with the available data, or the generated query fails, the response will contain an `error` field:
```json theme={null}
{
"error": "This question cannot be answered with the available data."
}
```
Other possible errors include request timeouts and response size limits.
`ask` uses an AI model to translate your question into SQL. For best results, be specific about the data you want, time ranges, and how results should be grouped or sorted. To inspect the data model itself (entities, fields, relationships), use `get_data_model` instead.
## get\_retailers
List the retailers available to your account as structured rows. By default returns only the retailers enabled for your team (each with a Subtotal Link URL); pass `include="all"` to list every active retailer in the catalog, annotated with whether it's enabled for your team.
**Parameters:**
| Name | Type | Required | Description |
| :-------- | :----- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `include` | string | No | `enabled` (default) — only the retailers enabled for your team, each with a `link_url`. `all` — every active retailer in the catalog, each annotated with an `enabled` flag and a `link_url` (present only when enabled). These are the only accepted values. |
**Response format:**
```json theme={null}
{
"retailers": [
{
"retailer_id": "walmart",
"name": "Walmart",
"images": {
"logo": { "light": ".../light/logo.svg", "dark": ".../dark/logo.svg" },
"icon": { "light": ".../light/icon.svg", "dark": ".../dark/icon.svg" }
},
"status": "active",
"enabled": true,
"link_url": "https://link.subtotal.com/a1b2c3d4"
}
]
}
```
**Fields:**
| Field | Description |
| :------------ | :--------------------------------------------------------------------------------------------------------- |
| `retailer_id` | Retailer slug — the exact value used to filter purchase data by retailer (e.g. in `ask`). |
| `name` | Retailer display name. |
| `images` | Logo and icon URLs, each with a `light` and `dark` theme. |
| `status` | Platform status: `active`, `limited`, or `inactive`. |
| `enabled` | Whether this retailer is enabled for your team. |
| `link_url` | URL to launch Subtotal Link for the retailer. Present only when the retailer is enabled; `null` otherwise. |
This is the same retailer contract returned by the public `GET /retailers` API. With `include="all"`, `link_url` is `null` for retailers not enabled for your team.
## get\_data\_model
Returns the data model for your purchase data, including entities, fields, types, and relationships.
**Parameters:** None
**Returns:** A plain-text data model describing six entities: `connection`, `purchase`, `item`, `retailer`, `product`, and `brand`.
If you're using `ask`, you typically don't need to call `get_data_model` — the server already has the data model when answering questions.
# Create and submit an app
Source: https://docs.subtotal.com/docs/partner-apps/create-and-review
Configure your listing, credentials, OAuth URLs, and review submission.
Once Subtotal authorizes your dashboard team to create apps, a team admin can create and manage them from **Apps → Created Apps**.
## Create an app
Select **Create app** and complete the form with production-quality information. The dashboard team that creates the app owns it; that ownership cannot be changed after creation.
### Listing information
| Field | Requirement |
| :------------------ | :-------------------------------------------------------------------------- |
| App name | A clear brand-facing name, up to 256 characters |
| Website URL | The app or partner website; HTTPS is required |
| Listing description | What the app does and why a brand should install it, up to 2,000 characters |
| App icon | JPG, PNG, or SVG; square; up to 5 MB |
Use an icon that remains recognizable at small sizes. The uploaded icon is shown on the reviewed listing, approved authorization screen, and installed-app page.
### OAuth URLs
The **Install URL** is a page you operate. When a brand opens it, your application should create state and PKCE values, then start a [top-level Subtotal authorization request](/docs/partner-apps/oauth).
Add every callback as an **Allow-listed redirect URL**. Subtotal performs an exact match during authorization.
* Add no more than 10 redirect URLs.
* Use HTTPS in hosted environments.
* HTTP is accepted only for loopback development hosts: `localhost`, `127.0.0.1`, or `::1`.
* Keep local and production callbacks as separate entries.
## Protect the app credentials
Subtotal creates two independent types of credentials.
### Client ID and client secret
The Client ID identifies the OAuth app and is not secret. The client secret authenticates your server to `/oauth/token` and `/oauth/revoke`.
* The client secret is shown only when the app is created or when you rotate it. Copy it immediately into a secrets manager.
* Never put the client secret in browser code, mobile apps, source control, screenshots, URLs, or logs.
* Subtotal stores only a one-way hash of the client secret and cannot recover it later.
* **Rotate client secret** invalidates the previous value immediately. Update your secret store and token-exchange service as one coordinated change.
### Webhook signing secret
The signing secret verifies events delivered to the app's webhook URL. It is separate from the OAuth client secret and can be revealed again from **Created Apps**.
When you rotate it, Subtotal returns the previous signing secret during a 24-hour overlap. Accept signatures made with either value during that window, then remove the previous value. See [Partner webhooks](/docs/partner-apps/webhooks).
## Install and test the app
Select **Install app** before submission. It opens your Install URL, from which you must start the normal authorization-code flow. Until approval:
* Only the dashboard team that owns the app can install it;
* The consent and installed-app pages identify it as a development app;
* An installation created while the app is a draft remains active during review;
* Submitted and rejected apps remain limited to the owning team and can continue using **Install app**.
## Submit and approve
Select **Submit for review** after the listing, Install URL, redirect URLs, and integration behavior are ready. Submitted configuration is read-only.
After approval:
* The reviewed listing becomes available to other brands under **Apps**;
* Other brands can install the app;
* The partner team's existing development installation is promoted in place, without creating a duplicate;
* The development label and **Install app** action in **Created Apps** disappear; and
* The configuration remains read-only in this onboarding release.
If Subtotal rejects a submission, review the notes, edit the app, save it back to draft, test again, and resubmit.
# Partner apps
Source: https://docs.subtotal.com/docs/partner-apps/introduction
Create an app that brands can install to connect with your platform.
Subtotal works with technology partners that help brands make more use of their retail purchase data. Loyalty platforms, review providers, customer engagement tools, analytics products, and other partners can create an app that a brand installs to securely connect its Subtotal data with their service.
Under the hood, the app requests access to a brand's data through the OAuth Authorization Code flow with PKCE, following OAuth 2.1 security practices. It can also receive the brand's selected webhook events at an endpoint operated by the partner.
## Become a Subtotal partner
To create an app, first sign up for the [Subtotal Dashboard](https://dashboard.subtotal.com). Once Subtotal authorizes your dashboard team to create apps, team admins can create and manage them under **Apps → Created Apps**.
Before approval, only the team that owns the app can install and test it. Once approved by Subtotal, other brands can discover and install it.
## Integration lifecycle
Enter the listing, OAuth, and optional webhook configuration in **Created Apps**. Subtotal assigns a Client ID, a one-time client secret, a webhook signing secret, and the app's fixed scopes.
Use **Install app** to run the real authorization-code flow with the dashboard team that owns the app. This development installation is available only to that team.
Subtotal reviews the listing and integration configuration. Submitted apps are read-only, so finish testing before you submit.
Approval publishes the reviewed listing. A brand starts at your Install URL, authorizes all three fixed permissions in Subtotal, and returns to an allow-listed redirect URI.
## Fixed permissions
Every partner app is assigned the same permission set. There are no broader scopes available to partners at this time.
| Scope | Access |
| :------------------------- | :------------------------------------------------------------------------------------------- |
| `connections:read` | View the brand's connections and receive configured connection events |
| `retailers:read` | Read the authorizing brand's [retailer catalog](/docs/api-reference/retailers/get-retailers) |
| `purchases:brand_products` | Receive configured `purchase.created` events containing brand products |
Omit `scope` to request the full set, or send all three scopes exactly once. Partner apps receive them as one fixed grant; subsets, additional scopes, and duplicates are rejected. The `purchases:brand_products` scope is required for a partner installation to subscribe to `purchase.created`.
Direct OAuth Bearer access to purchase list and detail endpoints is not available yet. Today, `purchases:brand_products` authorizes configured `purchase.created` webhook deliveries and limits their purchase data to brand products. Connection API access, OAuth identity, installation management, and partner webhooks are available today.
## Current limitations
* Apps under development can be installed only by the dashboard team that created them. Other brands can install an app only after Subtotal approves it.
* Submitted and approved app configuration is read-only in this onboarding release.
* Partner scopes are fixed; partners cannot add, remove, or negotiate individual permissions.
* Approval and rejection are performed by Subtotal. A rejected app can be edited and resubmitted; saving an edited rejection returns it to draft.
# OAuth authorization
Source: https://docs.subtotal.com/docs/partner-apps/oauth
Implement the authorization-code flow with PKCE for a partner app.
Partner apps use the OAuth Authorization Code flow with PKCE, following OAuth 2.1 security practices. The app is a confidential client: use PKCE for every authorization and authenticate token and revocation requests with HTTP Basic.
The authorization server is `https://oauth.subtotal.com`. Its current endpoints and capabilities are published at:
```text theme={null}
https://oauth.subtotal.com/.well-known/oauth-authorization-server
```
| Purpose | Endpoint | Authentication |
| :------------------------- | :--------------------- | :------------------------------------------------ |
| Start authorization | `GET /oauth/authorize` | Top-level browser navigation; no HTTP credentials |
| Exchange or refresh tokens | `POST /oauth/token` | HTTP Basic with the Client ID and client secret |
| Identify an installation | `GET /oauth/me` | Bearer access token |
| Revoke an installation | `POST /oauth/revoke` | HTTP Basic with the Client ID and client secret |
## 1. Request authorization
Generate a high-entropy `state` value and an RFC 7636 PKCE verifier for each attempt. Save both in a short-lived, server-side install session.
```js theme={null}
import { createHash, randomBytes } from "node:crypto";
const base64url = (value) =>
value.toString("base64url");
const state = base64url(randomBytes(32));
const codeVerifier = base64url(randomBytes(64));
const codeChallenge = base64url(
createHash("sha256").update(codeVerifier).digest()
);
```
Redirect the user agent with a **top-level browser navigation**. Do not embed Subtotal authorization in an iframe.
```text theme={null}
GET https://oauth.subtotal.com/oauth/authorize
?response_type=code
&client_id=YOUR_OAUTH_CLIENT_ID
&redirect_uri=https%3A%2F%2Fconnect.example.com%2Foauth%2Fsubtotal%2Fcallback
&resource=https%3A%2F%2Fapi.subtotal.com
&scope=connections%3Aread%20retailers%3Aread%20purchases%3Abrand_products
&state=YOUR_RANDOM_STATE
&code_challenge=YOUR_S256_CHALLENGE
&code_challenge_method=S256
```
| Query parameter | Required value or purpose |
| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ |
| `response_type` | Required. Use `code`. |
| `client_id` | Required. The Client ID shown in **Created Apps**. |
| `redirect_uri` | Required. One of the app's allow-listed redirect URIs. Matching is exact. |
| `resource` | Required. Use `https://api.subtotal.com` so the access token is valid for the Subtotal API. |
| `scope` | Optional. Omit to request the full set, or send `connections:read retailers:read purchases:brand_products` in any order, each exactly once. |
| `state` | Required. A high-entropy value unique to this authorization attempt. Validate it on return. |
| `code_challenge` | Required. The base64url-encoded SHA-256 digest of the PKCE verifier. |
| `code_challenge_method` | Required. Use `S256`. |
If a user belongs to multiple dashboard teams, Subtotal asks them to choose which team will grant access. The token response's `subtotal_client_id` identifies the selected team.
Partner apps do not negotiate permissions. Requests for a subset, an additional scope, or a duplicated scope value are rejected.
## 2. Handle the callback
Subtotal redirects the browser to the allow-listed URI after the brand approves, denies, or encounters an authorization error.
```text theme={null}
https://connect.example.com/oauth/subtotal/callback
?code=AUTHORIZATION_CODE
&state=YOUR_RANDOM_STATE
```
| Query parameter | When present |
| :-------------- | :----------------------------------------------------------------------------------------------------- |
| `code` | Approval. A temporary authorization code that expires after 10 minutes and can be exchanged only once. |
| `state` | Every redirect. The original value from the authorization request. |
| `error` | Denial or authorization failure. An OAuth error code is returned instead of `code`. |
Before exchanging the code:
1. If `error` is present, stop the flow and show an appropriate message.
2. Compare `state` to the value in the install session using a constant-time comparison.
3. Reject missing, expired, or mismatched state.
4. Load the corresponding PKCE verifier and delete the install session after use.
## 3. Exchange the authorization code
Call the token endpoint from your server. Supply the Client ID and client secret from **Created Apps** with HTTP Basic, and send form-encoded parameters.
```bash theme={null}
curl --request POST https://oauth.subtotal.com/oauth/token \
--user "$SUBTOTAL_OAUTH_CLIENT_ID:$SUBTOTAL_OAUTH_CLIENT_SECRET" \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode "code=$AUTHORIZATION_CODE" \
--data-urlencode 'redirect_uri=https://connect.example.com/oauth/subtotal/callback' \
--data-urlencode "code_verifier=$PKCE_CODE_VERIFIER"
```
| Form parameter | Required value or purpose |
| :-------------- | :-------------------------------------------------------------------- |
| `grant_type` | Required. Use `authorization_code`. |
| `code` | Required. The single-use authorization code returned to the callback. |
| `redirect_uri` | Required. The same redirect URI used in the authorization request. |
| `code_verifier` | Required. The original 43–128 character PKCE verifier. |
The response has this shape:
```json theme={null}
{
"access_token": "",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "",
"refresh_token_expires_in": 2592000,
"scope": "connections:read retailers:read purchases:brand_products",
"subtotal_client_id": ""
}
```
| Response field | Meaning |
| :------------------------- | :------------------------------------------------------------------------------------- |
| `access_token` | Bearer token for authorized Subtotal API requests. |
| `token_type` | Always `Bearer`. |
| `expires_in` | Access-token lifetime in seconds. Currently `900`. |
| `refresh_token` | Opaque token used to obtain the next access token. Store it encrypted. |
| `refresh_token_expires_in` | Seconds until the returned refresh token expires. Currently up to `2592000` (30 days). |
| `scope` | The fixed permissions granted to the app. |
| `subtotal_client_id` | The dashboard team that authorized the installation. |
A successful exchange activates the app installation for the selected dashboard team and starts configured webhook delivery.
### Identify the app and installation
The authorization flow uses two identifiers with different sources:
| Name | Source and meaning |
| :------------------- | :---------------------------------------------------------------------------------------------------------------- |
| `client_id` | The OAuth app's Client ID, shown in **Created Apps** and sent by your app during authorization and token exchange |
| `subtotal_client_id` | The authorizing dashboard team's ID, returned by Subtotal in the token response |
Store `subtotal_client_id` with the installation. It identifies the dashboard team that granted access; do not infer that team from the person who completed consent. An installation is unique to one app and dashboard team. Reauthorizing the same pair updates the existing installation instead of creating another one.
Each refresh-token rotation starts a new 30-day lifetime. Track expiry using the response values rather than hard-coded durations. Keep tokens encrypted on your server, out of browser storage, URLs, and logs.
## 4. Refresh access tokens
Use the current refresh token when the access token is near expiry. Authenticate the app with HTTP Basic and send form-encoded parameters.
```bash theme={null}
curl --request POST https://oauth.subtotal.com/oauth/token \
--user "$SUBTOTAL_OAUTH_CLIENT_ID:$SUBTOTAL_OAUTH_CLIENT_SECRET" \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode "refresh_token=$SUBTOTAL_REFRESH_TOKEN"
```
| Form parameter | Required value or purpose |
| :-------------- | :-------------------------------------------------------- |
| `grant_type` | Required. Use `refresh_token`. |
| `refresh_token` | Required. The current refresh token for the installation. |
A successful refresh preserves all three scopes and returns the same fields as the code exchange, with new tokens and expiry values. Refresh only once at a time per installation. Save all returned values in one atomic update before the next refresh, and always use the newest refresh token.
### Retries and recovery
After a timeout, network failure, or temporary server error, retry promptly with the same refresh token. If the first request succeeded, retries return the same replacement refresh token and a fresh access token.
The previous refresh token is retryable for **60 minutes from its rotation**, or until its replacement is used for another refresh, whichever comes first. Retries do not extend this window or the replacement's expiry.
Reusing an older token or retrying after that window returns `400 invalid_grant`, revokes the installation's refresh tokens, and disconnects the app, stopping new webhook delivery. Expired or revoked refresh tokens also return `invalid_grant`. On this error, stop retrying, pause background work, and ask the brand to authorize the app again.
### Upgrade an existing installation
Installations authorized before `retailers:read` was included need fresh consent. Existing access tokens keep their original permissions until expiry; refreshing does not add the new scope. Authorization codes and refresh tokens with the old two-scope grant return `invalid_grant`.
1. Have the brand click **Install app** again. Your Install URL must start a new authorization flow with all three scopes, or omit `scope`.
2. Have the brand select the same dashboard team and approve access.
3. Exchange the new code and replace the stored tokens for that installation.
This updates the existing installation. No uninstall is required. The new access token can call [GET /retailers](/docs/api-reference/retailers/get-retailers).
## 5. Recover installation identity
If the callback succeeds but your state-to-installation mapping is lost, call `/oauth/me` with the access token. This endpoint has no query or form parameters.
```bash theme={null}
curl https://oauth.subtotal.com/oauth/me \
--header "Authorization: Bearer $SUBTOTAL_ACCESS_TOKEN"
```
```json theme={null}
{
"subtotal_client_id": "",
"client_id": "",
"scope": "connections:read retailers:read purchases:brand_products"
}
```
## 6. Disconnect in Subtotal
A brand can open the installed app under **Apps**, select **Disconnect**, and type `DISCONNECT` to confirm. This removes the app's access to that brand's data and stops sending new events to the app.
Disconnecting revokes active refresh tokens and deactivates the installation. Already-issued access tokens remain valid until their 15-minute expiry. If a refresh fails with `invalid_grant`, stop background work and require reauthorization. To reconnect, send the brand through a new authorization flow.
## 7. Revoke from your service
If a brand uninstalls from your product, revoke its active refresh-token session. Authenticate with the app's current Client ID and client secret.
```bash theme={null}
curl --request POST https://oauth.subtotal.com/oauth/revoke \
--user "$SUBTOTAL_OAUTH_CLIENT_ID:$SUBTOTAL_OAUTH_CLIENT_SECRET" \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "token=$SUBTOTAL_REFRESH_TOKEN"
```
| Form parameter | Required value or purpose |
| :------------- | :-------------------------------------------------------------- |
| `token` | Required. The refresh token for the installation being revoked. |
Revocation returns `200` for known and unknown tokens. For a known token, it revokes active refresh tokens for that app and dashboard team, deactivates the Subtotal installation, and stops webhook delivery. Already-issued access tokens are self-contained and remain valid until their 15-minute expiry.
# Partner webhooks
Source: https://docs.subtotal.com/docs/partner-apps/webhooks
Configure app events and verify signed webhook deliveries.
A partner app sends events to a single webhook endpoint. Configure the endpoint and event types in **Created Apps**.
## Configure delivery
Enter a public HTTPS **Webhook URL**, then select any of the currently supported event types:
* `connection.activated`
* `connection.unauthenticated`
* `purchase.created`
See [Webhook event types](/docs/webhooks/events) for the complete payload schema for each event and [Identify the installing dashboard team](#identify-the-installing-dashboard-team) for how to determine which brand the event belongs to.
Webhook delivery begins after a brand successfully installs the app. Reauthorizing the same dashboard team updates the existing installation rather than creating a duplicate.
The app's fixed OAuth grant controls which events it can receive: `connections:read` permits the connection events, and `purchases:brand_products` is required for `purchase.created`.
## Identify the installing dashboard team
Partner deliveries add `subtotal_client_id` at the top level. Use it to route the event to the correct brand in your system.
```jsonc theme={null}
{
"type": "connection.activated",
"id": "",
"subtotal_client_id": "",
"payload": {
// Event-specific fields omitted
}
}
```
The event `id` is stable across delivery replays. Make handlers idempotent by deduplicating on that ID within the installing dashboard team.
## Verify every request
Use the app's signing secret to verify every delivery. See [Verifying webhook signatures](/docs/webhooks/verifying-signatures) for the shared signing protocol and implementation guidance.
Subtotal sends:
| Header | Value |
| :--------------------- | :--------------------------------------- |
| `X-Subtotal-Timestamp` | Unix timestamp in seconds |
| `X-Subtotal-Signature` | Lowercase hexadecimal HMAC-SHA256 digest |
Compute the HMAC over the timestamp, one period, and the **exact raw request body**:
```text theme={null}
signed_payload = X-Subtotal-Timestamp + "." + raw_body
```
```js theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifySubtotalWebhook({ rawBody, timestamp, signature, secrets }) {
if (!/^\d+$/.test(timestamp) || !/^[0-9a-f]{64}$/.test(signature)) {
return false;
}
const received = Buffer.from(signature, "hex");
const signedPayload = `${timestamp}.${rawBody}`;
return secrets.some((secret) => {
const expected = createHmac("sha256", secret)
.update(signedPayload, "utf8")
.digest();
return expected.length === received.length &&
timingSafeEqual(expected, received);
});
}
```
Also reject timestamps outside your replay-tolerance window and acknowledge valid requests with a `2xx` response quickly. Parse JSON only after signature verification.
### Rotate the signing secret
Use **Rotate signing secret** in **Created Apps**. Deploy the new value and keep the returned previous value as a verifier for no more than 24 hours. Pass both values in `secrets` during the overlap; afterward, remove the previous value.
## Simulated events
Enable **Receive simulated events** to deliver events created through Subtotal's [Event Simulator](/docs/event-simulator) to installed copies of this app. Leave it disabled if your endpoint should receive only non-simulated activity.
When enabled, simulated events matching the app's selected event types are sent to the same webhook endpoint as other events.
**Receive simulated events** must be disabled for approved apps.
## Disconnect behavior
When a brand disconnects the app, Subtotal immediately stops webhook delivery for that brand. Other brands' installations are unaffected. A later reinstall resumes delivery using the app's current webhook configuration.
# Subtotal API
Source: https://docs.subtotal.com/docs/subtotal-api
Make HTTP requests in your preferred programming language.
## Introduction
The Subtotal API is built on REST principles, ensuring a stateless, scalable, and reliable interface for interacting with merchant accounts on behalf of customers.
## API Reference
Our [API reference](/docs/api-reference) provides all the necessary details to integrate Subtotal's features into your application, including endpoint descriptions, request and response formats, and examples.
## Base URL
Append an endpoint to the base URL root address to form a complete request URL.
```
https://api.subtotal.com
```
## Authentication
All requests must include an API key for authentication.
Include the following header in each request:
```json theme={null}
headers = {
"X-Api-Key": "{yourkeyvalue}"
}
```
## Authorization
We reserve the *Authorization* header for endpoints that require a [connection token](/docs/api-reference/connections/create-connection-token).
```json theme={null}
headers = {
"Authorization": "Bearer: {connection_token}"
}
```
# Getting started with Subtotal Connect
Source: https://docs.subtotal.com/docs/subtotal-connect/introduction
Apps, libraries, and SDKs that simplify integrating with Subtotal.
## Overview
Subtotal Connect is a collection of apps, libraries, and SDKs that simplify integrating with Subtotal. It provides frontend components you can use to add buttons that start the Subtotal Link flow and let end users link retailer accounts, disconnect accounts, and reauthenticate accounts that are no longer signed in.
Subtotal Connect components rely on [Subtotal Link](/docs/subtotal-link/introduction) for the actual account-linking experience. Configure your brand and retailers in the [Subtotal Dashboard](https://dashboard.subtotal.com/link) before using Connect components.
## What’s available
Today Subtotal Connect includes the **Shopify app**, which offers two components:
1. **Theme App Block** — An app block you can add in the Shopify theme editor (e.g. on the Account page). It’s fully customizable via theme editor settings and [custom CSS](/docs/subtotal-connect/shopify).
2. **Profile UI Extension** — An extension that works with the newer Shopify customer accounts, so customers can manage their linked retailers directly in their profile.
Use one or both depending on your theme and whether you use the new customer accounts experience.
## Next steps
* **[Shopify](/docs/subtotal-connect/shopify)** — Overview of the Subtotal Connect Shopify app, its two components, connection tags, and how to customize the theme app block.
# Shopify
Source: https://docs.subtotal.com/docs/subtotal-connect/shopify
# Subtotal Connect for Shopify
The Subtotal Connect Shopify app adds account-linking to your store so customers can connect their retailer accounts (e.g., Amazon, Walmart, Sephora) and share verified purchases with you. Install the app from the Shopify App Store, then add one or both of the components below to your store.
***
## Components
The app includes two components. Use one or both depending on your theme and whether you use the new Shopify customer accounts experience.
### Theme app block
A block you add in the Shopify theme editor—typically on the Account page or another customer-facing page. It shows a list of supported retailers so customers can link, disconnect, or reauthenticate accounts. The theme app block is fully customizable: you can change copy and colors in the theme editor, and use custom CSS for advanced styling (see [Customizing the theme app block](#customizing-the-theme-app-block) below).
### Profile UI extension
An extension that works with the newer [Shopify customer accounts](https://help.shopify.com/en/manual/customers/customer-accounts). When enabled, customers can manage their linked retailer accounts from their profile without needing the theme app block on a specific page. The profile extension uses Shopify’s default styling for the customer account area.
***
## Connection tags
Connection tags let you label the connections your store creates, so you can group them when you report on connections. A tag you set is returned on the connection by [Get a connection](/docs/api-reference/connections/get-connection). Tags are yours to define — Subtotal never interprets them.
The app supports three tag names: `utm_campaign`, `utm_source`, and `utm_medium`. All three are optional. Leave them blank and connections are created without tags.
### Where you set them
| Where the customer starts | Where you set the tags |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Theme app block | The block's settings in the theme editor |
| Profile UI extension | The extension's settings in the customer accounts editor |
| A Subtotal link URL you hand out | Query parameters on the URL itself — see [Tagging a link URL](/docs/subtotal-connect/shopify-storefront-api#tagging-a-link-url) |
Each surface is configured separately, so you can tell connections from your storefront apart from connections from a customer's profile:
```
Theme app block utm_source = storefront
Profile extension utm_source = account
```
### What to expect
* Tags are recorded when a connection is **created**. Editing a setting later doesn't change connections that already exist.
* A value longer than 256 characters is dropped rather than truncated.
* Only the three names above are accepted. Anything else is ignored.
* Storefront page URLs are never read for tags. A customer who arrives on your site with UTM parameters and later links an account is not tagged from those parameters — only the values you set, or the ones on a Subtotal link URL, are used.
Don't put personal information in tags. Tag values are visible wherever you group connections, and they aren't intended for customer data.
***
## Customizing the theme app block
The following sections apply only to the **theme app block**. They do not apply to the profile UI extension.
### Quick Start
1. In your Shopify admin, go to **Online Store** > **Themes**
2. Click **Customize** on your active theme
3. Navigate to the page where Subtotal Connect is placed (typically the Account page)
4. Click on the **Subtotal Connect** block in the sidebar
5. Adjust settings to match your brand
6. Click **Save** to apply changes
### Theme editor settings
The following settings are available for the theme app block in the Shopify theme editor:
| Setting | Description | Default |
| -------------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
| **Heading** | Main title displayed above the retailer list | "Link your retailer accounts" |
| **Description** | Subtitle text explaining the feature | "Connect your accounts to earn points when you shop in-store." |
| **Background color** | App block container background | `#ffffff` |
| **Text color** | Primary text color for headings and retailer names | `#000000` |
| **Accent color** | Used for buttons, loading animation, and hover effects | `#000000` |
| **Custom CSS** | Advanced CSS overrides using CSS variables | Empty |
| **utm\_campaign** | Connection tag attached to connections started from this block | Empty |
| **utm\_source** | Connection tag attached to connections started from this block | Empty |
| **utm\_medium** | Connection tag attached to connections started from this block | Empty |
***
### CSS Variables Reference
For advanced customization, use the **Custom CSS** field in the theme app block settings to override any style. All styles are controlled through CSS custom properties that you can override.
#### Color Variables
```css theme={null}
--subtotal-connect-bg /* App block background */
--subtotal-connect-text-primary /* Primary text color */
--subtotal-connect-text-secondary /* Secondary/muted text color */
--subtotal-connect-border-color /* Container border color */
--subtotal-connect-accent-color /* Accent/highlight color */
--subtotal-connect-card-bg /* Card background */
--subtotal-connect-card-border-color /* Card border color */
```
#### Button Variables
```css theme={null}
--subtotal-connect-btn-primary-bg /* "Connect" button background */
--subtotal-connect-btn-primary-text /* "Connect" button text */
--subtotal-connect-btn-secondary-bg /* "Sign in"/"Disconnect" button background */
--subtotal-connect-btn-secondary-text /* "Sign in"/"Disconnect" button text */
--subtotal-connect-btn-secondary-border /* "Sign in"/"Disconnect" button border */
```
#### Typography Variables
```css theme={null}
--subtotal-connect-heading-size /* Heading font size (default: 28px) */
--subtotal-connect-description-size /* Description font size (default: 16px) */
--subtotal-connect-card-name-size /* Retailer name font size (default: 18px) */
--subtotal-connect-card-status-size /* Status text font size (default: 14px) */
--subtotal-connect-btn-font-size /* Button font size (default: 14px) */
```
#### Spacing Variables
```css theme={null}
--subtotal-connect-spacing /* Container padding (default: 24px) */
--subtotal-connect-card-gap /* Gap between cards (default: 16px) */
--subtotal-connect-card-padding /* Padding inside cards (default: 16px) */
```
#### Border Radius Variables
```css theme={null}
--subtotal-connect-container-radius /* Container border radius (default: 12px) */
--subtotal-connect-card-radius /* Card border radius (default: 12px) */
--subtotal-connect-btn-radius /* Button border radius (default: 9999px) */
--subtotal-connect-logo-radius /* Logo border radius (default: 8px) */
```
#### Effect Variables
```css theme={null}
--subtotal-connect-card-shadow /* Card shadow (default: subtle shadow) */
```
***
### Premade CSS Themes
Copy and paste these themes into the **Custom CSS** field (theme app block settings) for instant styling. Each theme is designed to work as a complete style override.
#### Clean Minimal
A clean, borderless design with subtle shadows and refined spacing.
```css theme={null}
.subtotal-connect {
--subtotal-connect-bg: #fafafa;
--subtotal-connect-card-bg: #ffffff;
--subtotal-connect-card-shadow: 0 1px 3px rgba(0,0,0,0.08);
--subtotal-connect-card-border-color: transparent;
}
.retailer-card {
border: none !important;
}
.subtotal-connect__heading {
letter-spacing: -0.025em;
}
```
#### Modern Gradient
A contemporary theme with a gradient header accent and modern styling.
```css theme={null}
.subtotal-connect {
--subtotal-connect-text-primary: #1f2937;
--subtotal-connect-card-border-color: #e5e7eb;
}
.subtotal-connect__header {
padding-bottom: 1.5rem;
border-bottom: 3px solid;
border-image: linear-gradient(90deg, #667eea, #764ba2) 1;
margin-bottom: 0.5rem;
}
.retailer-card__button--primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
}
.retailer-card__button--primary:hover {
filter: brightness(1.1) !important;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
```
#### Dark Mode
A striking dark theme perfect for stores with dark aesthetics.
```css theme={null}
.subtotal-connect {
--subtotal-connect-bg: #18181b;
--subtotal-connect-text-primary: #fafafa;
--subtotal-connect-text-secondary: rgba(250, 250, 250, 0.7);
--subtotal-connect-card-bg: #27272a;
--subtotal-connect-card-border-color: #3f3f46;
--subtotal-connect-border-color: #3f3f46;
--subtotal-connect-accent-color: #a78bfa;
--subtotal-connect-btn-primary-bg: #fafafa;
--subtotal-connect-btn-primary-text: #18181b;
--subtotal-connect-btn-secondary-bg: transparent;
--subtotal-connect-btn-secondary-text: #f87171;
--subtotal-connect-btn-secondary-border: #f87171;
}
.retailer-card__logo {
background-color: #ffffff;
padding: 4px;
}
```
#### Soft & Rounded (Warm)
A friendly, approachable design with soft colors and generous rounding.
```css theme={null}
.subtotal-connect {
--subtotal-connect-bg: #fef3c7;
--subtotal-connect-text-primary: #92400e;
--subtotal-connect-text-secondary: rgba(146, 64, 14, 0.7);
--subtotal-connect-card-bg: #fffbeb;
--subtotal-connect-card-border-color: #fcd34d;
--subtotal-connect-accent-color: #f59e0b;
--subtotal-connect-btn-primary-bg: #f59e0b;
--subtotal-connect-btn-primary-text: #ffffff;
--subtotal-connect-btn-secondary-text: #d97706;
--subtotal-connect-btn-secondary-border: #d97706;
--subtotal-connect-card-radius: 20px;
--subtotal-connect-btn-radius: 16px;
border-radius: 24px;
}
```
#### Professional Blue
A corporate, trustworthy design with blue accents.
```css theme={null}
.subtotal-connect {
--subtotal-connect-bg: #f0f9ff;
--subtotal-connect-text-primary: #0c4a6e;
--subtotal-connect-text-secondary: rgba(12, 74, 110, 0.7);
--subtotal-connect-card-bg: #ffffff;
--subtotal-connect-card-border-color: #bae6fd;
--subtotal-connect-accent-color: #0284c7;
--subtotal-connect-btn-primary-bg: #0284c7;
--subtotal-connect-btn-primary-text: #ffffff;
--subtotal-connect-btn-secondary-text: #0369a1;
--subtotal-connect-btn-secondary-border: #0369a1;
border: 2px solid #0284c7;
border-radius: 12px;
}
.subtotal-connect__heading {
color: #0284c7;
}
```
#### Elegant Card
A sophisticated design with a prominent left border accent.
```css theme={null}
.subtotal-connect {
--subtotal-connect-text-primary: #1e293b;
--subtotal-connect-accent-color: #6366f1;
--subtotal-connect-btn-primary-bg: #6366f1;
--subtotal-connect-card-border-color: #e2e8f0;
border-left: 4px solid #6366f1;
border-radius: 0 12px 12px 0;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
}
.retailer-card:hover {
transform: translateX(4px) !important;
}
```
#### Playful Pop
A fun, eye-catching design with bold shadows and vibrant colors.
```css theme={null}
.subtotal-connect {
--subtotal-connect-bg: #fdf4ff;
--subtotal-connect-text-primary: #701a75;
--subtotal-connect-accent-color: #d946ef;
--subtotal-connect-card-bg: #ffffff;
--subtotal-connect-btn-primary-bg: #d946ef;
--subtotal-connect-btn-secondary-text: #a21caf;
--subtotal-connect-btn-secondary-border: #a21caf;
border: 3px solid #18181b;
border-radius: 16px;
box-shadow: 6px 6px 0 #18181b;
}
.retailer-card {
border: 2px solid #18181b !important;
box-shadow: 3px 3px 0 #18181b !important;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.retailer-card:hover {
transform: translate(-2px, -2px) !important;
box-shadow: 5px 5px 0 #18181b !important;
}
.retailer-card__button {
border: 2px solid #18181b !important;
}
```
#### Nature Green
An earthy, eco-friendly design with natural green tones.
```css theme={null}
.subtotal-connect {
--subtotal-connect-bg: #f0fdf4;
--subtotal-connect-text-primary: #166534;
--subtotal-connect-text-secondary: rgba(22, 101, 52, 0.7);
--subtotal-connect-card-bg: #ffffff;
--subtotal-connect-card-border-color: #bbf7d0;
--subtotal-connect-accent-color: #22c55e;
--subtotal-connect-btn-primary-bg: #16a34a;
--subtotal-connect-btn-primary-text: #ffffff;
--subtotal-connect-btn-secondary-text: #15803d;
--subtotal-connect-btn-secondary-border: #15803d;
}
```
***
### Advanced Customization Examples
#### Custom Fonts
Import and apply custom fonts from Google Fonts:
```css theme={null}
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
.subtotal-connect {
font-family: 'Inter', sans-serif;
}
.subtotal-connect__heading {
font-family: 'Inter', sans-serif;
}
```
#### Custom Entrance Animation
Replace the default fade-in with a custom animation:
```css theme={null}
.subtotal-connect {
animation: custom-slide-up 0.6s ease-out !important;
}
@keyframes custom-slide-up {
0% {
opacity: 0;
transform: translateY(30px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
```
#### Staggered Card Animation
Add staggered animations to cards as they appear:
```css theme={null}
.retailer-card {
animation: card-fade-in 0.4s ease-out;
animation-fill-mode: both;
}
.retailer-card:nth-child(1) { animation-delay: 0.1s; }
.retailer-card:nth-child(2) { animation-delay: 0.2s; }
.retailer-card:nth-child(3) { animation-delay: 0.3s; }
.retailer-card:nth-child(4) { animation-delay: 0.4s; }
@keyframes card-fade-in {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
```
#### Glassmorphism Effect
Create a modern frosted glass appearance:
```css theme={null}
.subtotal-connect {
--subtotal-connect-bg: rgba(255, 255, 255, 0.7);
--subtotal-connect-card-bg: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.retailer-card {
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.4) !important;
}
```
#### Center-Aligned Header
Change the header alignment to centered:
```css theme={null}
.subtotal-connect__header {
text-align: center;
align-items: center;
}
```
#### Adjust Typography Sizes
Make the app block more compact or larger:
```css theme={null}
/* Compact version */
.subtotal-connect {
--subtotal-connect-heading-size: 22px;
--subtotal-connect-description-size: 14px;
--subtotal-connect-card-name-size: 16px;
--subtotal-connect-card-status-size: 12px;
--subtotal-connect-spacing: 16px;
--subtotal-connect-card-padding: 12px;
}
/* Larger version */
.subtotal-connect {
--subtotal-connect-heading-size: 36px;
--subtotal-connect-description-size: 18px;
--subtotal-connect-card-name-size: 22px;
--subtotal-connect-spacing: 32px;
}
```
#### Remove Card Shadows and Borders
For a flatter, more minimal look:
```css theme={null}
.subtotal-connect {
--subtotal-connect-card-shadow: none;
}
.retailer-card {
border: none !important;
}
```
#### Add Container Border
Wrap the app block in a border:
```css theme={null}
.subtotal-connect {
border: 1px solid var(--subtotal-connect-border-color);
border-radius: var(--subtotal-connect-container-radius);
}
```
#### Custom Mobile Breakpoint Styles
Fine-tune the app block for mobile devices:
```css theme={null}
@media (max-width: 480px) {
.subtotal-connect {
--subtotal-connect-heading-size: 22px;
--subtotal-connect-description-size: 14px;
--subtotal-connect-spacing: 16px;
}
.retailer-card__button {
padding: 10px 16px !important;
font-size: 13px !important;
}
}
```
***
### Key Elements Reference
Use these class names to target specific elements in your Custom CSS for the theme app block:
| Element | Class | Description |
| ----------------- | ----------------------------------- | -------------------------------- |
| Container | `.subtotal-connect` | Main app block wrapper |
| Inner Container | `.subtotal-connect__container` | Flex container for layout |
| Header | `.subtotal-connect__header` | Contains heading and description |
| Heading | `.subtotal-connect__heading` | Main title (h1) |
| Description | `.subtotal-connect__description` | Subtitle paragraph |
| Retailers Grid | `.subtotal-connect__retailers-list` | Grid container for cards |
| Loading Animation | `.subtotal-connect__loading` | Loading dots container |
| Loading Dot | `.subtotal-connect__dot` | Individual loading dot |
| Card | `.retailer-card` | Individual retailer card |
| Card Info | `.retailer-card__info` | Logo and text container |
| Logo Container | `.retailer-card__logo-container` | Logo wrapper |
| Logo | `.retailer-card__logo` | Retailer logo image |
| Name/Status | `.retailer-card__name-status` | Text container |
| Retailer Name | `.retailer-card__name` | Retailer name (h3) |
| Status | `.retailer-card__status` | Connection status text |
| Button | `.retailer-card__button` | Action button |
| Primary Button | `.retailer-card__button--primary` | Connect button |
| Secondary Button | `.retailer-card__button--secondary` | Sign in/Disconnect button |
***
### Troubleshooting (theme app block)
#### CSS Changes Not Appearing
1. Make sure you clicked **Save** after adding custom CSS
2. Clear your browser cache and hard refresh (Cmd/Ctrl + Shift + R)
3. Check for CSS syntax errors (missing semicolons, brackets)
4. Use `!important` if your styles are being overridden by theme CSS
#### App Block Not Displaying
1. Ensure you're logged in as a customer when viewing the account page
2. Check that the Subtotal app is properly configured
3. Verify the block is added to the correct page template
#### Styles Conflicting with Theme
If your Shopify theme's CSS conflicts with the app block:
```css theme={null}
/* Increase specificity */
#subtotal-connect-app-block.subtotal-connect {
/* Your styles here */
}
/* Or use !important (use sparingly) */
.subtotal-connect__heading {
color: #000000 !important;
}
```
#### Custom CSS Field Too Small
The Custom CSS textarea in Shopify's theme editor is small. For complex themes:
1. Write your CSS in a separate code editor
2. Copy and paste into the Custom CSS field
3. Keep a backup of your custom CSS locally
***
### Need Help?
If you need assistance with custom designs or have questions about the theme app block:
* Contact our support team at [support@subtotal.com](mailto:support@subtotal.com)
* Include screenshots of your current setup and desired outcome
* Provide your store URL for faster troubleshooting
# Shopify Storefront API
Source: https://docs.subtotal.com/docs/subtotal-connect/shopify-storefront-api
App-proxy endpoints for building custom Subtotal Link entrypoints from a Shopify storefront.
The Subtotal Connect Shopify app exposes a small set of JSON endpoints through Shopify's [app proxy](https://shopify.dev/docs/apps/build/online-store/display-dynamic-data). You can call them from any storefront page to build custom UI — for example, a "Link your Walmart account" button on a marketing page, a CTA inside a loyalty widget, or an integration with a third-party storefront framework — without using the [theme app block](/docs/subtotal-connect/shopify#theme-app-block) or [profile UI extension](/docs/subtotal-connect/shopify#profile-ui-extension).
If the theme app block or profile UI extension meets your needs, use those instead — they handle UI, state, and styling for you. The Storefront API is for cases where you need a custom entrypoint or a different layout.
## Base path
All endpoints are mounted at `/apps/subtotal/*` on the merchant's storefront domain. Fetch them with a relative path:
```js theme={null}
fetch('/apps/subtotal/retailers')
```
## Authentication
You do **not** send an API key. Shopify's app proxy signs every request server-side, so calls only work when they originate from a page on the merchant's storefront.
When a customer is signed in, Shopify automatically appends `logged_in_customer_id` as a query parameter when proxying the request to the Subtotal Connect backend. Endpoints that operate on a specific customer (`/retailers`, `/connections`, `/disconnect-connection`, `/visibility`, `/link/{linkId}`) require the customer to be signed in. If they aren't, those endpoints return `400` or redirect to the Shopify customer login.
## Endpoints
| Method | Path | Purpose |
| ------ | -------------------------------------- | ------------------------------------------------------------ |
| `GET` | `/apps/subtotal/configuration` | Check whether the shop has Subtotal Connect configured |
| `GET` | `/apps/subtotal/visibility` | Check visibility rules for the current customer |
| `GET` | `/apps/subtotal/retailers` | List retailers with the current customer's connection status |
| `POST` | `/apps/subtotal/connections` | Create or reuse a connection, return a Subtotal Link URL |
| `POST` | `/apps/subtotal/disconnect-connection` | Disconnect an existing connection |
| `GET` | `/apps/subtotal/link/{linkId}` | One-shot redirect into Subtotal Link for a single retailer |
***
### `GET /apps/subtotal/configuration`
Returns `200` if the shop has configured Subtotal Connect (i.e. a Subtotal API key is stored for the shop). Returns `404` otherwise. Use this to feature-detect before rendering your UI.
**Response**
```json theme={null}
{ "status": "success" }
```
**Example**
```js theme={null}
async function isSubtotalConnectConfigured() {
const response = await fetch('/apps/subtotal/configuration');
return response.ok;
}
```
***
### `GET /apps/subtotal/visibility`
Returns the visibility decision for the current customer based on the rules the merchant has configured in the Shopify admin (global allow-list of emails/domains, plus a per-retailer allow-list). Use this if you want your custom entrypoint to respect the same gating as the theme app block.
**Response**
```json theme={null}
{
"blockVisible": true,
"restrictedRetailerIds": []
}
```
* `blockVisible` — `false` when the merchant's global visibility rules hide Subtotal Connect for this customer.
* `restrictedRetailerIds` — IDs the merchant has hidden from this specific customer. Filter these out of any retailer list you render.
**Example**
```js theme={null}
async function checkVisibility() {
const response = await fetch('/apps/subtotal/visibility');
if (!response.ok) return { blockVisible: true, restrictedRetailerIds: [] };
return response.json();
}
```
***
### `GET /apps/subtotal/retailers`
Returns the list of retailers supported for the shop, with the current customer's connection (if any) attached to each retailer.
**Response**
```json theme={null}
{
"retailers": [
{
"retailer_id": "walmart",
"name": "Walmart",
"link_url": "https://link.subtotal.com/zklQOnlG",
"images": {
"logo": { "light": "...", "dark": "..." },
"icon": { "light": "...", "dark": "..." }
},
"connection": {
"connection_id": "01K...",
"retailer_id": "walmart",
"status": "active"
}
}
]
}
```
`retailer_id` is a lowercase slug (e.g. `walmart`, `amazon`, `kroger`). The `link_url` path suffix is a short mixed-case alphanumeric `linkId` (e.g. `zklQOnlG`) used by [`/apps/subtotal/link/{linkId}`](#get-apps-subtotal-link-linkid).
`connection` is omitted when the customer has never linked the retailer. `status` is one of:
| Status | Meaning | Suggested button text |
| ------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| *(no `connection`)* | The customer has never linked this retailer | "Link Walmart" |
| `initialized` | Connection created but not yet authenticated | "Link Walmart" |
| `disconnected` | Customer disconnected the account | "Link Walmart" |
| `revoked` | Retailer revoked access | "Link Walmart" |
| `unauthenticated` | Previously authenticated, needs the customer to sign in again | "Login to sync new purchases" |
| `active` | Authenticated and collecting purchases | "Disconnect", "Purchase on Walmart" (link to your brand's products on the retailer), or hide the button |
Treat `initialized`, `disconnected`, and `revoked` the same as "no connection" — show a fresh **Link** action that starts a new linking flow.
**Example**
```js theme={null}
async function getRetailers() {
const response = await fetch('/apps/subtotal/retailers');
if (!response.ok) throw new Error(`retailers fetch failed: ${response.status}`);
const data = await response.json();
return data.retailers;
}
```
***
### `POST /apps/subtotal/connections`
Creates (or reuses) a Subtotal connection for the current customer and returns a URL that opens Subtotal Link with the connection pre-bound. Redirect the customer to `link_url` to start the linking flow.
**Request body**
| Field | Type | Required | Description |
| ----------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `retailerId` | string | yes | `retailer_id` from `/apps/subtotal/retailers` |
| `retailerLinkUrl` | string | yes | `link_url` from the same retailer object |
| `redirectUrl` | string | yes | URL Subtotal Link should send the customer back to after they finish (e.g. `window.location.href`) |
| `connectionId` | string | no | Existing connection ID — pass this when reauthenticating an `unauthenticated` connection |
| `tags` | object | no | Connection tags to record on a new connection. Only `utm_campaign`, `utm_source`, and `utm_medium` are accepted; other names are ignored. Values are trimmed, and a value over 256 characters is dropped. Ignored when `connectionId` is set, since the connection already exists |
**Response**
```json theme={null}
{
"link_url": "https://link.subtotal.com/zklQOnlG?connection_id=01K...&redirect_url=https%3A%2F%2Fshop.example.com%2Faccount"
}
```
**Example** — a Connect button that opens Subtotal Link for a single retailer:
```js theme={null}
async function startLinkFlow(retailer) {
const body = {
retailerId: retailer.retailer_id,
retailerLinkUrl: retailer.link_url,
redirectUrl: window.location.href,
};
if (retailer.connection) {
// Reauthenticate an existing connection
body.connectionId = retailer.connection.connection_id;
}
const response = await fetch('/apps/subtotal/connections', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`failed to create connection: ${response.statusText}`);
const { link_url } = await response.json();
window.location.href = link_url;
}
```
***
### `POST /apps/subtotal/disconnect-connection`
Disconnects an existing connection. After this returns, the connection's status moves to `disconnected` and Subtotal stops collecting purchases for it.
**Request body**
| Field | Type | Required | Description |
| -------------- | ------ | -------- | ----------------------------------------------- |
| `connectionId` | string | yes | `connection_id` from `/apps/subtotal/retailers` |
**Response**
```json theme={null}
{ "success": true }
```
**Example**
```js theme={null}
async function disconnect(connectionId) {
const response = await fetch('/apps/subtotal/disconnect-connection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ connectionId }),
});
if (!response.ok) throw new Error(`failed to disconnect: ${response.statusText}`);
}
```
***
### `GET /apps/subtotal/link/{linkId}`
One-shot redirect endpoint. Resolves the retailer with the given `linkId` (the last path segment of a retailer's `link_url`), creates or finds the customer's connection, and `302`s into Subtotal Link. If the customer isn't signed in, they're redirected to Shopify's customer login first and bounced back here on success.
Useful for plain `` entrypoints where you don't want to run any client-side JavaScript.
**Query parameters**
| Param | Required | Description |
| -------------- | -------- | ------------------------------------------------------------------------------ |
| `redirect_url` | no | Where to send the customer after they finish (defaults to the shop's homepage) |
| `utm_campaign` | no | Connection tag — see [Tagging a link URL](#tagging-a-link-url) |
| `utm_source` | no | Connection tag — see [Tagging a link URL](#tagging-a-link-url) |
| `utm_medium` | no | Connection tag — see [Tagging a link URL](#tagging-a-link-url) |
**Example**
```html theme={null}
Link your Walmart account
```
#### Tagging a link URL
Add `utm_campaign`, `utm_source`, or `utm_medium` to the URL and they're recorded as [connection tags](/docs/subtotal-connect/shopify#connection-tags) on the connection this endpoint creates. Use them to tell apart the places you send customers from:
```html theme={null}
Link your Walmart account
```
The tags are returned on the connection by [Get a connection](/docs/api-reference/connections/get-connection), and you can group connection metrics by them.
Only those three names are read; any other query parameter is ignored. Values are trimmed, and a value over 256 characters is dropped. Tags are recorded only when this endpoint creates a new connection — if the customer already has one for that retailer, they're redirected into Subtotal Link and the existing connection keeps the tags it was created with.
## Putting it together
A minimal custom Connect button — feature-detect, fetch the customer's Walmart connection state, and render the right entrypoint for the current status. For linking and reauthenticating, [`/apps/subtotal/link/{linkId}`](#get-apps-subtotal-link-linkid) does the work in one step, so the button can be a plain ``. Only disconnect needs a JS click handler.
```js theme={null}
const configured = await fetch('/apps/subtotal/configuration').then(r => r.ok);
if (!configured) return;
const { retailers } = await fetch('/apps/subtotal/retailers').then(r => r.json());
const walmart = retailers.find(r => r.retailer_id === 'walmart');
const status = walmart.connection?.status;
const linkId = walmart.link_url.split('/').pop();
const redirectUrl = encodeURIComponent(window.location.href);
const slot = document.querySelector('#subtotal-entrypoint');
if (status === 'active') {
// Already linked — render a Disconnect button
const btn = document.createElement('button');
btn.textContent = 'Disconnect Walmart';
btn.addEventListener('click', async () => {
await fetch('/apps/subtotal/disconnect-connection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ connectionId: walmart.connection.connection_id }),
});
window.location.reload();
});
slot.appendChild(btn);
} else {
// Not linked (or needs reauth) — a plain link to the convenience redirect
// covers both cases: it creates a connection if needed, or reuses the
// existing one for unauthenticated reauth.
const a = document.createElement('a');
a.href = `/apps/subtotal/link/${linkId}?redirect_url=${redirectUrl}`;
a.textContent = status === 'unauthenticated'
? 'Login to sync new purchases'
: 'Link Walmart';
slot.appendChild(a);
}
```
If you don't need to inspect the customer's current connection status (for example, on a marketing page where you just want a "Link your Walmart account" CTA), skip the `/retailers` call entirely and hard-code the `linkId`:
```html theme={null}
Link your Walmart account
```
# Subtotal Dashboard
Source: https://docs.subtotal.com/docs/subtotal-dashboard
Manage your Subtotal account, API keys, Link experience, webhooks, and integrations in one place.
## Overview
The [Subtotal Dashboard](https://dashboard.subtotal.com) is the web-based control center for your Subtotal account.
Use the Subtotal Dashboard to configure how your brand uses Subtotal, access data and insights, manage API access, set up webhooks and integrations, and test flows with simulated data.
## Signing up
Sign up at [https://dashboard.subtotal.com/sign-up](https://dashboard.subtotal.com/sign-up).
## Subtotal Link
The first thing you'll want to do after signing up is configure Subtotal Link to use your brand name, icon, and retailers.
Check out our docs on [Subtotal Link](/docs/subtotal-link) to get started.
## API keys
Create and manage API keys in the [Subtotal Dashboard](https://dashboard.subtotal.com/api-keys).
Authenticate your requests to the Subtotal API with a valid API key. See [Authentication](/docs/api-reference/introduction#authentication) in the API reference.
## Webhooks
In the Dashboard you add **webhook destinations**—the URLs where Subtotal sends event payloads when connections and purchases occur. Go to [Webhooks](https://dashboard.subtotal.com/webhooks) (or **Developers** → **Webhooks**), click **Add destination**, enter your HTTPS endpoint URL, choose which events to subscribe to, and save. The Dashboard shows a **signing secret** for each destination; use it to [verify webhook signatures](/docs/webhooks/verifying-signatures) in your endpoint. For each destination you can turn **simulated events** on or off so the [Event Simulator](/docs/event-simulator) can send test events to that URL. See [Webhooks](/docs/webhooks/introduction) for full details and [event types](/docs/webhooks/events).
## Event Simulator
The Dashboard includes an **Event Simulator** that allows you to simulate real-world events such as shoppers linking an account or buying a product.
See [Event Simulator](/docs/event-simulator) to learn how to simulate events.
## Integrations
Connect Subtotal to third-party platforms under **Integrations**.
See the relevant docs for each integration.
| Integration | What it does |
| :---------------------------------------- | :---------------------------------------------------------------------------------- |
| [Attentive](/docs/integrations/attentive) | Create segments and trigger journeys with Subtotal events. |
| [Braze](/docs/integrations/braze) | Deliver real-time purchase data as custom events for campaigns and Canvas journeys. |
| [Klaviyo](/docs/integrations/klaviyo) | Create segments and trigger flows with Subtotal events. |
| [Rivo](/docs/integrations/rivo) | Reward customers for linking accounts and making purchases. |
| [Smile](/docs/integrations/smile) | Reward customers for linking accounts and making purchases. |
| [Yotpo](/docs/integrations/yotpo) | Reward customers for linking accounts and making purchases. |
# Getting started with Subtotal Link
Source: https://docs.subtotal.com/docs/subtotal-link/introduction
Customers use Subtotal Link to connect their accounts.
## Overview
Subtotal Link is the interface that allows consumers to securely connect their accounts—from retailers like Amazon, Walmart, and Sephora—to apps and websites they use.
It provides a consistent, trusted experience for account linking across all supported retailers.
Subtotal Link is the only available method for connecting accounts and is required for all Subtotal integrations.
## Configuration
Sign in to the [Subtotal Dashboard](https://dashboard.subtotal.com/link) and configure the account linking experience for your users.
***
### Brand name
Your brand name appears throughout the Subtotal Link experience and tells users who they are allowing to access their purchase data.
It should clearly identify your brand so customers understand who they’re sharing their retail data with.
***
### Brand icon
Your brand icon is displayed alongside your brand name to help users quickly recognize you.
Use a simple, high-contrast logo that remains legible at small sizes to reinforce brand trust during the linking flow.
***
### Retailers
Select the retailers you want your program to support.
Customers can link their accounts from these retailers to share verified purchase data with your brand.
***
## Link an account
For campaigns and customer profiles, create a [customer Link token](/docs/subtotal-link/link-tokens) from your backend and send the consumer their URL. It opens the branded retailer picker and can be reused across visits and enabled retailers.
If your app already creates retailer connections, continue using [connection tokens](/docs/api-reference/connections/create-connection-token). Existing static and `/select?client_id=…` URLs are deprecated and will be removed in a future release.
# Customer Link tokens
Source: https://docs.subtotal.com/docs/subtotal-link/link-tokens
Create, reuse, and manage a consumer’s Subtotal Link URL.
A Link token gives a consumer a reusable URL for connecting their retailer accounts. Create the token from your backend, then send the consumer to their URL. They can choose from your enabled retailers and return to connect additional accounts.
The token is opaque, not a JWT. It has the prefix `lt_v1_` followed by 43 base64url characters and contains no readable customer information. It expires after 30 days by default, with a configurable lifetime of 1–365 whole days.
## Create a token and construct the URL
Call [Create a Link token](/docs/api-reference/link-tokens/create-link-token) from your server. Authenticate with `X-Api-Key`, or a brand-authorized OAuth access token in `Authorization: Bearer …`. OAuth callers need `connections:write` to create or revoke and `connections:read` to list. Your Subtotal client must be active.
Keep API credentials on your backend and verify the consumer's identity before issuing their URL.
```bash theme={null}
curl --request POST 'https://api.subtotal.com/link-tokens' \
--header "X-Api-Key: $SUBTOTAL_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"customer_id": "customer-example-001",
"email": "consumer@example.com",
"tags": {"source": "app"},
"expires_in_days": 30
}'
```
A successful request returns `201 Created`.
```json theme={null}
{
"customer_id": "customer-example-001",
"status": "active",
"created_at": "2026-09-04T12:00:00Z",
"expires_at": "2026-10-04T12:00:00Z",
"revoked_at": null,
"link_token": "lt_v1_<43-character-secret>"
}
```
Use the returned `link_token` to construct the URL:
```typescript theme={null}
const subtotalLinkUrl = `https://link.subtotal.com/${created.link_token}`;
```
### Return to your app
The optional `redirect_url` query parameter sets where the consumer goes when they select **Exit**, including after successful linking or from an unavailable-link screen. URL-encode its value. Without it, the success screen asks the consumer to close the window.
Only `redirect_url` is carried through retailer selection; other top-level query parameters are ignored.
### Link result
When the consumer selects **Exit**, the redirect URL includes `subtotal_link_result`:
* `success`: retailer authentication succeeded; purchase import may still be in progress.
* `cancelled`: the consumer exited before completing linking.
* `timeout`: the authentication session timed out or expired.
* `invalid`: the link is invalid, expired, or revoked.
* `error`: another linking or service failure occurred.
The result reflects the current attempt, including any retry. Existing callback parameters and fragments are preserved; an existing `subtotal_link_result` is replaced. Closing the tab does not guarantee a redirect. Use the result for your return screen and the API or webhooks to confirm connection state.
## Inputs and attribution
| Field | Required | Rules |
| ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `customer_id` | Yes | Your stable consumer identifier, 1–64 characters; surrounding whitespace is removed. Use an opaque ID, not an email address. |
| `email` | Yes | Valid email, up to 254 characters; normalized before storage. |
| `mobile` | No | Valid international phone number, including country code, up to 16 characters. |
| `tags` | No | Up to 10 string pairs; names 1–64 characters and values 1–256 characters after trimming. Use campaign/source metadata, never consumer PII. |
| `expires_in_days` | No | Whole days, 1–365; defaults to 30. |
These values are immutable. Create a new token to change them or extend the lifetime.
Selecting a retailer reuses the newest non-revoked connection for your client, consumer, and retailer, or creates one if needed. Email, mobile, and tags apply only to new connections; existing connection metadata is unchanged.
## Store and reuse a token
Store the `link_token` and `expires_at` in your backend, associated with the consumer. Use the same token to construct links for future visits or to revoke access to that URL. You do not need a new token for each visit or retailer; multiple tokens for the same consumer can also be active at once.
## Idempotency and token recovery
`X-Subtotal-Idempotency-Key` is optional. Include it to safely retry a creation request without issuing another token. Use a nonempty value up to 128 characters, and reuse the same key for retries. Keys are scoped to your client.
* The same key and normalized inputs return the original token and creation response, including the original expiry.
* The same key with different inputs returns `409 Conflict`.
* A new key creates a new token, even for the same consumer.
* Without a key, each successful request creates a new token.
If you supplied a key, an exact replay can recover the original token, but does not extend its lifetime or undo revocation. It returns the original creation response; use the metadata list for current status. To replace an expired or revoked token, use a new key or omit the header.
## List metadata
[List Link tokens](/docs/api-reference/link-tokens/get-link-tokens) requires `customer_id` and optionally filters by `status=active`, `expired`, or `revoked`:
```bash theme={null}
curl --get 'https://api.subtotal.com/link-tokens' \
--header "X-Api-Key: $SUBTOTAL_API_KEY" \
--data-urlencode 'customer_id=customer-example-001' \
--data-urlencode 'status=active'
```
The response is `{ "link_tokens": [...] }`, with `customer_id`, `status`, `created_at`, `expires_at`, and `revoked_at` on each item. It does not return the raw token.
## Revoke or replace a URL
[Revoke a Link token](/docs/api-reference/link-tokens/revoke-link-token) from your backend using the stored token:
```bash theme={null}
curl --request DELETE "https://api.subtotal.com/link-tokens/$SUBTOTAL_LINK_TOKEN" \
--header "X-Api-Key: $SUBTOTAL_API_KEY"
```
Revocation is permanent. The API returns `204 No Content`, including for an already-revoked or expired token. An unknown token or one owned by another client returns `404`.
Revoking or expiring a Link token prevents future exchanges. It does **not** disconnect existing retailer connections or invalidate a connection JWT already issued for the 30-minute Link flow.
## Expired and unavailable links
An unknown, expired, or revoked link displays:
> This link is no longer active. Ask the brand that sent it for a new link.
Create a new token from your backend and provide the consumer with the replacement URL. Previously shared URLs do not change when you create a new token.
## Handling URLs securely
Anyone with the URL can use it, so keep stored tokens secure and share links only with the intended consumer. Do not put PII in URL parameters or tags.
# Legacy Link URLs
Source: https://docs.subtotal.com/docs/subtotal-link/link-urls
Compatibility reference for existing static per-retailer URLs.
Static Link URLs are deprecated and will be removed in a future release. Use [customer Link tokens](/docs/subtotal-link/link-tokens) instead. Existing links continue to work for now.
## Overview
Each retailer's legacy **Link URL** launches Subtotal Link for that retailer. This page documents existing integrations, not the recommended setup for new ones.
## Query parameters
Existing static URLs accept the following query parameters. Customer Link tokens also support [`redirect_url`](/docs/subtotal-link/link-tokens#return-to-your-app); send customer context in the authenticated token-creation body instead.
| Parameter | Description | Example |
| :-------------- | :------------------------------------------------------------------------------------------------------ | :------------------------------------- |
| `customer_id` | A unique identifier used to map connections to customer records across external systems. | `8a46e581-48ba-498a-9ad0-2ee72582e1af` |
| `redirect_url` | The URL that customers should be redirected to after successfully linking their account. | `https://example.com/profile` |
| `connection_id` | The identifier of an existing connection (used to reauthenticate a connection or reconnect an account). | `01KFCQJPY9GN5BBB8ERHEWAZ7J` |
Redirects include a [`subtotal_link_result`](/docs/subtotal-link/link-tokens#link-result) indicating how the attempt ended.
Here’s an example of a complete Link URL that includes `customer_id` and `redirect_url`:
```
https://link.subtotal.com/tWGE4TJM
?customer_id=8a46e581-48ba-498a-9ad0-2ee72582e1af
&redirect_url=https%3A%2F%2Fexample.com%2Fprofile
```
# Legacy retailer selection
Source: https://docs.subtotal.com/docs/subtotal-link/retailer-selection
Compatibility reference for the existing /select URL.
The `/select?client_id=…` URL is deprecated and will be removed in a future release. Use [customer Link tokens](/docs/subtotal-link/link-tokens) instead. Existing links continue to work for now.
## Overview
Each [Link URL](/docs/subtotal-link/link-urls) launches Subtotal Link for one specific retailer. The **retailer-selection URL** is a hosted page that shows customers every retailer your program supports — under your brand's name and icon — and lets them choose which account to connect.
Picking a retailer starts the standard Subtotal Link flow for that retailer. Nothing else about the linking experience changes: the customer sees the same consent and login screens, and lands on your `redirect_url` after a successful connection.
## URL format
```
https://link.subtotal.com/select
?client_id=01KFCQK5H2M4R8W3T7Y9NBVCXZ
&customer_id=8a46e581-48ba-498a-9ad0-2ee72582e1af
&redirect_url=https%3A%2F%2Fexample.com%2Fprofile
```
## Query parameters
| Parameter | Required | Description | Example |
| :------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------- |
| `client_id` | Yes | Identifies your brand and determines the branding and retailers shown. Provided by Subtotal — reach out to your Subtotal contact if you don't have it. | `01KFCQK5H2M4R8W3T7Y9NBVCXZ` |
| `customer_id` | No | A unique identifier used to map connections to customer records across external systems. Behaves exactly as on [Link URLs](/docs/subtotal-link/link-urls#query-parameters). | `8a46e581-48ba-498a-9ad0-2ee72582e1af` |
| `redirect_url` | No | The URL that customers should be redirected to after successfully linking their account. Behaves exactly as on [Link URLs](/docs/subtotal-link/link-urls#query-parameters). | `https://example.com/profile` |
URL-encode the `redirect_url` value — especially when it carries query parameters of its own.
Legacy integrations may also pass other query parameters through to the chosen retailer's Link flow. Do not add email, mobile, or other PII to URLs. New integrations send customer context in the authenticated [token-creation body](/docs/subtotal-link/link-tokens#inputs-and-attribution).
## What your customers see
The page lists the retailers configured for your program, each as an identical option. When a customer picks one:
1. The standard Subtotal Link flow for that retailer begins — consent, then the retailer login.
2. If the `customer_id` you passed already has a connection for that retailer, it is reused automatically instead of creating a duplicate — picking an already-connected retailer simply re-links it.
3. After a successful connection, the customer is redirected to your `redirect_url` (or shown the standard success screen if you didn't provide one).
The page never displays connection status — every retailer looks the same regardless of whether the customer has linked it before.
# Configuring webhook destinations
Source: https://docs.subtotal.com/docs/webhooks/configuration
Set up webhook destinations in the Subtotal Dashboard.
Configure webhook destinations in the [Subtotal Dashboard](https://dashboard.subtotal.com) so Subtotal knows where to send events.
## Create a webhook destination
Sign in to the Subtotal Dashboard and select Webhooks from the side menu.
Click *Add Webhook Destination* and specify a name and URL.
You will receive a **signing secret** for the destination. Store this secret securely and use it to [verify webhook signatures](/docs/webhooks/verifying-signatures) for every request.
Keep your signing secret private.
Select the *Configure* option on your new webhook destination. Enable the events that you'd like this destination to receive.
## Requirements
* Your endpoint must be **HTTPS**.
* Your endpoint should respond with a **2xx** status quickly. Subtotal may retry on failure.
* Implement [signature verification](/docs/webhooks/verifying-signatures) to ensure requests are from Subtotal.
# Event types
Source: https://docs.subtotal.com/docs/webhooks/events
Subscribe to system events to receive real-time notifications.
## Overview
Webhooks let you subscribe to Subtotal system events and receive real-time HTTP requests when those events occur. Use webhooks to keep your systems in sync with connection and purchase activity.
## Properties included with every event
These properties are included with each webhook request:
| Key | Type | Description |
| :------ | :----- | :------------------------- |
| type | string | The webhook event type |
| id | string | Identifier for the webhook |
| payload | string | The payload of the webhook |
Below are all supported event types and their corresponding payload structures.
## connection.activated
Connections are `activated` when a customer successfully links their account.
| Key | Type | Description |
| :-------------------------------- | :----- | :--------------------------------------------- |
| payload.connection.connection\_id | string | Identifier for the connection |
| payload.connection.customer\_id | string | Identifier for the associated customer |
| payload.connection.retailer\_id | string | Identifier for the retailer in the connection |
| payload.connection.email | string | Email address of the associated customer |
| payload.connection.mobile | string | Mobile phone number of the associated customer |
| payload.connection.status | string | Current status of the connection |
**Example `connection.activated` event**
```json theme={null}
{
"type": "connection.activated",
"id": "01KFJSR44CPH867805V8GJ9VCY",
"payload": {
"connection": {
"connection_id": "01K8BPF23CCZ2FK50STB8EGWAV",
"customer_id": "01K8BPF2393S9VB6JQT99GK447",
"retailer_id": "target",
"email": null,
"mobile": null,
"status": "active"
}
}
}
```
***
## connection.unauthenticated
Connections move from `activated` to `unauthenticated` when a customer needs to re-authenticate their account. For example, when a customer changes their password with the retailer.
| Key | Type | Description |
| :-------------------------------- | :----- | :--------------------------------------------- |
| payload.connection.connection\_id | string | Identifier for the connection |
| payload.connection.customer\_id | string | Identifier for the associated customer |
| payload.connection.retailer\_id | string | Identifier for the retailer in the connection |
| payload.connection.email | string | Email address of the associated customer |
| payload.connection.mobile | string | Mobile phone number of the associated customer |
| payload.connection.status | string | Current status of the connection |
**Example `connection.unauthenticated` event**
```json theme={null}
{
"type": "connection.unauthenticated",
"id": "01KFJSR44CPH867805V8GJ9VCY",
"payload": {
"connection": {
"connection_id": "01K8BPF23CCZ2FK50STB8EGWAV",
"customer_id": "01K8BPF2393S9VB6JQT99GK447",
"retailer_id": "target",
"email": null,
"mobile": null,
"status": "unauthenticated"
}
}
}
```
***
## connection.disconnected
Connections move to `disconnected` status when a customer disconnects their account.
| Key | Type | Description |
| :-------------------------------- | :----- | :--------------------------------------------- |
| payload.connection.connection\_id | string | Identifier for the connection |
| payload.connection.customer\_id | string | Identifier for the associated customer |
| payload.connection.retailer\_id | string | Identifier for the retailer in the connection |
| payload.connection.email | string | Email address of the associated customer |
| payload.connection.mobile | string | Mobile phone number of the associated customer |
| payload.connection.status | string | Current status of the connection |
**Example `connection.disconnected` event**
```json theme={null}
{
"type": "connection.disconnected",
"id": "01KFJSR44CPH867805V8GJ9VCY",
"payload": {
"connection": {
"connection_id": "01K8BPF23CCZ2FK50STB8EGWAV",
"customer_id": "01K8BPF2393S9VB6JQT99GK447",
"retailer_id": "target",
"email": null,
"mobile": null,
"status": "disconnected"
}
}
}
```
***
## connection.profile.created
Sent the first time Subtotal captures a consumer's profile for a connection — their identity details from the linked retailer account, together with purchase metrics scoped to your brands.
Unlike the other `connection.*` events, the profile payload is **flat**: its fields sit directly under `payload`, not under `payload.connection`.
Profile events are only delivered for **active** connections. While a connection is `unauthenticated` or `disconnected` you will not receive profile events for it; if the profile changed in the meantime, the connection receives a single catch-up event when it is re-linked (alongside `connection.activated`).
| Key | Type | Description |
| :---------------------------------- | :----- | :----------------------------------------------------------------------------- |
| payload.connection\_id | string | Identifier for the connection |
| payload.first\_name | string | Consumer's first name (`null` if unavailable) |
| payload.last\_name | string | Consumer's last name (`null` if unavailable) |
| payload.email | string | Consumer's email address (`null` if unavailable) |
| payload.mobile | string | Consumer's mobile phone number (`null` if unavailable) |
| payload.postal\_code | string | Consumer's postal code (`null` if unavailable) |
| payload.account\_created\_date | string | When the consumer's retailer account was created (ISO 8601; `null` if unknown) |
| payload.total\_purchases | number | All-time number of purchases on the connected account |
| payload.last\_purchase\_date | string | Date of the most recent purchase (ISO 8601; `null` if none) |
| payload.brand\_purchases | number | Number of those purchases you are authorized to see (matching your brands) |
| payload.last\_brand\_purchase\_date | string | Date of the most recent brand purchase (ISO 8601; `null` if none) |
| payload.brand\_purchase\_rate | number | `brand_purchases` ÷ `total_purchases`, from `0.0` to `1.0` |
**Example `connection.profile.created` event**
```json theme={null}
{
"type": "connection.profile.created",
"id": "01KFJSR44CPH867805V8GJ9VCY",
"payload": {
"connection_id": "01K8BPF23CCZ2FK50STB8EGWAV",
"first_name": "Jessica",
"last_name": "Smith",
"email": "jessica@acme.com",
"mobile": "+123456789",
"postal_code": "84101",
"account_created_date": "2024-03-12T00:00:00Z",
"total_purchases": 42,
"last_purchase_date": "2026-01-15T14:30:00Z",
"brand_purchases": 7,
"last_brand_purchase_date": "2026-01-14T09:45:00Z",
"brand_purchase_rate": 0.17
}
}
```
***
## connection.profile.updated
Sent when a previously captured profile changes — for example an updated email or postal code, or when new purchases shift the brand-purchase metrics. The payload is identical in shape to `connection.profile.created`. Like `connection.profile.created`, this event is only delivered while the connection is active; changes that happen while a connection is `unauthenticated` or `disconnected` arrive as one catch-up event on re-link.
| Key | Type | Description |
| :---------------------------------- | :----- | :----------------------------------------------------------------------------- |
| payload.connection\_id | string | Identifier for the connection |
| payload.first\_name | string | Consumer's first name (`null` if unavailable) |
| payload.last\_name | string | Consumer's last name (`null` if unavailable) |
| payload.email | string | Consumer's email address (`null` if unavailable) |
| payload.mobile | string | Consumer's mobile phone number (`null` if unavailable) |
| payload.postal\_code | string | Consumer's postal code (`null` if unavailable) |
| payload.account\_created\_date | string | When the consumer's retailer account was created (ISO 8601; `null` if unknown) |
| payload.total\_purchases | number | All-time number of purchases on the connected account |
| payload.last\_purchase\_date | string | Date of the most recent purchase (ISO 8601; `null` if none) |
| payload.brand\_purchases | number | Number of those purchases you are authorized to see (matching your brands) |
| payload.last\_brand\_purchase\_date | string | Date of the most recent brand purchase (ISO 8601; `null` if none) |
| payload.brand\_purchase\_rate | number | `brand_purchases` ÷ `total_purchases`, from `0.0` to `1.0` |
**Example `connection.profile.updated` event**
```json theme={null}
{
"type": "connection.profile.updated",
"id": "01KFJSR44CPH867805V8GJ9VCY",
"payload": {
"connection_id": "01K8BPF23CCZ2FK50STB8EGWAV",
"first_name": "Jessica",
"last_name": "Smith",
"email": "jessica.smith@acme.com",
"mobile": "+123456789",
"postal_code": "84101",
"account_created_date": "2024-03-12T00:00:00Z",
"total_purchases": 43,
"last_purchase_date": "2026-02-02T18:05:00Z",
"brand_purchases": 8,
"last_brand_purchase_date": "2026-02-02T18:05:00Z",
"brand_purchase_rate": 0.19
}
}
```
***
## purchase.created
The `purchase.created` event is sent when a new purchase is received from a customer's connected retailer account.
We recommend using this event when you need to process customer purchases for your use case.
| Key | Type | Description |
| :-------------------------------------------- | :----- | :------------------------------------------------------------------------------------------------------------- |
| payload.connection.connection\_id | string | Identifier for the connection |
| payload.connection.customer\_id | string | Identifier for the associated customer |
| payload.connection.retailer\_id | string | Identifier for retailer in the connection |
| payload.connection.email | string | Email address of the associated customer |
| payload.connection.mobile | string | Mobile phone number of the associated customer |
| payload.connection.status | string | Current status of the connection |
| payload.purchase.purchase\_id | string | Identifier for the purchase |
| payload.purchase.date | string | Date of the purchase (ISO 8601) |
| payload.purchase.item\_count | number | The number of items in the purchase |
| payload.purchase.subtotal | number | The subtotal of the purchase |
| payload.purchase.tax | number | The sales tax that was paid |
| payload.purchase.total | number | Total amount of the purchase |
| payload.purchase.items\[].item\_id | string | Identifier for the item |
| payload.purchase.items\[].price | number | Price of the item |
| payload.purchase.items\[].quantity | number | Quantity as shown on the receipt; items sold by weight show 1 |
| payload.purchase.items\[].unit\_quantity | number | Actual quantity purchased; fractional for items sold by weight, so `price × unit_quantity` is the amount spent |
| payload.purchase.items\[].product.product\_id | string | Identifier for the product |
| payload.purchase.items\[].product.name | string | The name of the product |
| payload.purchase.items\[].product.description | string | Description of the item |
| payload.purchase.items\[].product.upc | string | Universal identifier for the product (UPC) |
| payload.purchase.items\[].product.brand | string | The brand associated with the product |
**Example `purchase.created` event**
```json theme={null}
{
"id": "01J51S0JYV6N7K1030CV1ZBDOW",
"type": "purchase.created",
"payload": {
"connection": {
"connection_id": "01J51S0JYV6N7K1030CV1ZKSCA",
"customer_id": "01K8HDK6Y9FXM7ES4NJSHZDEKF",
"retailer_id": "walmart",
"email": "jessica@acme.com",
"mobile": "+123456789",
"status": "active"
},
"purchase": {
"purchase_id": "01J51S0JYV6N7K1030CV1ZKSJH",
"date": "2025-10-08T14:23:00Z",
"total": 47.85,
"subtotal": 43.50,
"tax": 4.35,
"items": [
{
"item_id": "01JV7ZDWC9GN1VPR41K3BY08X9",
"price": 5.99,
"quantity": 2,
"unit_quantity": 2,
"product": {
"product_id": "01J51S0JYV6N7K1030CV1ZKSCA",
"name": "Sparkling Water 12pk",
"description": "Sparkling Water 12-Pack - 12 fl oz bottles",
"upc": "001234567890",
"brand": "la-croix"
}
},
{
"item_id": "01K3KS9Q4522E3HG444E4XH1R0",
"price": 12.99,
"quantity": 1,
"unit_quantity": 1,
"product": {
"product_id": "01J51S0JYV6N7K1030CV1ZKSCA",
"name": "Trail Mix",
"description": "Trail Mix Family Size",
"upc": "009876543210",
"brand": "harvest-one"
}
},
{
"item_id": "01JV7ZDWC9GN1VPR41K3BY08X9",
"price": 8.50,
"quantity": 1,
"unit_quantity": 2.18,
"product": {
"product_id": "01J51S0JYV6N7K1030CV1ZKSCA",
"name": "Organic Cherries",
"description": "Organic Cherries, sold by the pound",
"upc": "011122233344",
"brand": "harvest-one"
}
}
]
}
}
}
```
# Verifying signatures
Source: https://docs.subtotal.com/docs/webhooks/verifying-signatures
Verify that webhook requests are from Subtotal and have not been tampered with.
You should verify the authenticity of each webhook request using the following headers.
| Key | Type | Description |
| :------------------- | :----- | :----------------------------------- |
| X-Subtotal-Signature | string | HMAC of payload using shared secret |
| X-Subtotal-Timestamp | string | The timestamp of the webhook request |
### Step 1: Prepare the `signed_payload` string
Concatenate the following to create the `signed_payload` string:
* The timestamp (as a string)
* A `.` character
* The request body (as a string)
### Step 2: Compute the `expected_signature`
Compute the `expected_signature` using the HMAC-SHA256 hash function.
Use the webhook destination's `signing_secret` as the key, and use the `signed_payload` string as the message.
For a directly configured destination, the `signing_secret` is returned when the destination is created from **Webhooks**. For a partner app, reveal or rotate the separate app signing secret from **Created Apps**. See [Partner webhooks](/docs/partner-apps/webhooks).
### Step 3: Compare the signatures
Compare the `expected_signature` to the `X-Subtotal-Signature`.
To prevent replay attacks, compare the received timestamp to the current time and reject requests outside your tolerance window.
To protect against timing attacks, use a constant-time string comparison.