Ask and metrics
Query your purchase data in plain language and get dashboard metrics.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:
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?”
- Questions must be plain English — embedded SQL will be rejected
- Only
SELECTqueries are generated — your data is never modified - Results are limited to 100 rows and 5 MB
- A 5-second execution timeout is enforced
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_metric
Get a Subtotal Dashboard metric for a date range, computed the way the dashboard computes it, with a link to that page. Prefer it overask for these metrics; use ask for everything else.
Parameters:
Metrics:
Example:
Metrics that need analytics return an error on plans without it. Windows must start in 2000 or later. Your team’s access scope applies, as it does for
ask.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.Connections
Find connections and read their history.list_connections
API: GET /connections List your connections, newest first. Every filter is optional. For counts or breakdowns across many connections, useask instead. Requires the connections:read OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns error_category: "insufficient_scope".
Parameters:
Response format:
has_more is true, call again with starting_after set to the last connection_id you received.
These are the same connections as the public
GET /connections API, except email and mobile are always null (use the API for contact details) and simulated test connections are listed only with include_simulated, which needs the simulator:read scope (the API always includes them). Without that scope, include_simulated returns error_category: "insufficient_scope". Deleted connections aren’t listed.get_connection
API: GET /connections/{connection_id} Get one connection: its customer ID, retailer, status, creation date and tags. Requires theconnections:read OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns error_category: "insufficient_scope".
Parameters:
Response format: a single connection, in the same shape as one entry of
list_connections.
This is the same connection as the public
GET /connections/{connection_id} API, except email and mobile are always null. A connection that doesn’t exist, belongs to another account, or has been deleted returns a “not found” error, and so does a simulated test connection unless the connector has the simulator:read scope.get_connection_activity
API: GET /connections/{connection_id}/activity Get a connection’s whole activity log, newest first: when it was created and activated, when it became unauthenticated or was disconnected, and when its customer ID, email or mobile changed. Each entry says who made the change. Pass aconnection_id from list_connections or an ask result, for example after asking “Which of my connections became unauthenticated this week?”. Requires the connections:read OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns error_category: "insufficient_scope".
Parameters:
Response format:
These are the same entries as the Activity tab in the dashboard and the public
GET /connections/{connection_id}/activity API. A connection that doesn’t exist, belongs to another account, or has been deleted returns a “not found” error.Purchases
Read one purchase and its items.get_purchase
API: GET /purchases/{purchase_id} Get one purchase with its items: totals, purchase type, status, and each item’s price, quantity and product. Pass apurchase_id from an ask result, for example after asking “What were my 10 largest orders last month?”.
Parameters:
Response format:
This is the same purchase as the public
GET /purchases/{purchase_id} API, filtered by your team’s access scope: items outside it, and sensitive items, are left out. A purchase that doesn’t exist or isn’t visible to your account returns a “not found” error.Purchase status values
Purchase status values
ask can also query by status, for example: “How many purchases are shipped?”status is the purchase status the retailer last showed. The value is a string, or null when unknown.The possible string values are:pendingshippedready_for_pickupcompletedcancelled
null.completed and cancelled are final. Once a purchase reaches either status, its status does not change again.Products
Browse and search your brand catalog.list_products
API: GET /products List or search your brand catalog: every active product of the brands you subscribe to, whether or not consumers have bought it yet, ordered byproduct_id. These are the same products as the Products page in the dashboard. For sales figures across products, use ask instead.
Parameters:
Response format:
get_product
API: GET /products/{product_id} Get one product from your brand catalog, in the same shape as alist_products row. Pass a product_id from list_products or an ask result.
Parameters:
A product that doesn’t exist, is inactive, or isn’t one of your brands’ products returns a “not found” error.
Product groups
Organise your catalog into groups and change their products.list_product_groups
API: GET /product-groups List the groups you organise your brand catalog into, ordered by name, each with how many products it holds. These are the same groups as the Products page in the dashboard; eachlist_products row names its groups in groups. Takes no parameters. To see a group’s products, pass its product_group_id to list_products as group_id. Requires the product_groups:read OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns error_category: "insufficient_scope".
Response format:
create_product_group
API: POST /product-groups Create an empty product group. Returns the group in the same shape as alist_product_groups row. Requires the product_groups:write OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns error_category: "insufficient_scope".
Parameters:
rename_product_group
API: PATCH /product-groups/{product_group_id} Rename a product group. Its products stay in it. Requires theproduct_groups:write OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns error_category: "insufficient_scope".
Parameters:
delete_product_group
API: DELETE /product-groups/{product_group_id} Delete a product group. Its products are not deleted. The tool is marked destructive, so assistants usually ask you to confirm first. Requires theproduct_groups:write OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns error_category: "insufficient_scope".
Parameters:
update_product_group_products
API: PUT /product-groups/{product_group_id}/products Add and remove products in a group in one call. Returns the group with its newproduct_count. Requires the product_groups:write OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns error_category: "insufficient_scope".
Parameters:
Only products from your brand catalog can be added. If any product to add isn’t in it, nothing changes and the tool returns a “not in your catalogue” error. A group that isn’t yours returns a “not found” error.
Retailers
See and configure the retailers available for linking.get_retailers
API: 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); passinclude="all" to list every active retailer in the catalog, annotated with whether it’s enabled for your team.
Requires the retailers:read OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns error_category: "insufficient_scope".
Parameters:
Response format:
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.update_retailer
API: PATCH /retailers/{retailer_id} Update a retailer for your account: enable or disable it, and set who sees it on the Subtotal Link retailer-selection page. Every argument exceptretailer_id is optional, and omitted ones are left unchanged, but a call must set at least one of enabled, link_visibility or link_audience. Requires the retailers:write OAuth scope; a connector without it gets error_category: "insufficient_scope".
- Visibility needs the retailer enabled: either already, or with
enabled: truein the same call. link_audiencereplaces the stored lists. Send both lists to keep entries you want.restrictedneeds an audience: at least one email or domain, counting lists already stored.- Disabling takes no visibility settings:
enabled: falsecan’t be combined withlink_visibilityorlink_audience.
Example — enable and restrict to one domain in a single call:
enabled: false, link_url, link_visibility and link_audience come back null.
Returns the same row shape as
get_retailers and the public PATCH /retailers/{retailer_id} API. Repeating a call returns the current row. These return an error field with error_category: "invalid_input": an unknown retailer_id, no fields to update, visibility settings for a retailer that isn’t enabled, visibility settings with enabled: false, or restricted with no audience. Invalid emails or domains, more than 100 entries in a list, or a link_visibility other than the three values are also rejected.Event simulator
Create test connections and events to try your webhooks or integration without a real shopper: the Event Simulator dashboard actions. Events go to webhook destinations that accept simulated events, markedis_simulated. Each tool works only on your simulated connections, so real connections never change.
simulate_connection
API: POST /simulated-connections Simulate a shopper linking a retailer account: creates an active simulated connection and sendsconnection.activated. Every call creates a new connection. Returns the connection in the same shape as get_connection, with is_simulated: true and email and mobile always null. Read it back with get_connection, or list_connections with include_simulated; both need the simulator:read scope. Requires the simulator:write OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns error_category: "insufficient_scope".
Parameters:
An unknown
retailer_id returns an error and creates nothing. To send a purchase.created event, use simulate_purchase.simulate_purchase
API: POST /simulated-connections/{connection_id}/purchases Simulate a purchase on one of your simulated connections, with random products from the connection’s retailer, dated now. Sendspurchase.created. Every call creates a new purchase. Returns its purchase_id; read it with get_purchase. Requires the simulator:write OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns error_category: "insufficient_scope".
Parameters:
Response format:
A real, deleted or unknown connection returns a “not found” error. The purchase includes at least one of your brands’ products when the retailer sells one. If it sells none and your account sees only your brands’ products,
get_purchase returns “not found” for it: the purchase was still created and its event sent, so don’t retry.set_simulated_status
API: PATCH /simulated-connections/{connection_id} Set one of your simulated connections toactive or unauthenticated, sending connection.activated or connection.unauthenticated, to test how your integration handles a shopper losing and regaining their retailer login. Setting the status it already has sends the event again. Returns the connection, as simulate_connection does. Requires the simulator:write OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns error_category: "insufficient_scope".
Parameters:
A real, deleted or unknown connection returns a “not found” error and nothing changes.
delete_simulated_connection
API: DELETE /simulated-connections/{connection_id} Delete one of your simulated connections with its simulated purchases, to clean up test data. It sends no event and can’t be undone. The tool is marked destructive, so assistants usually ask you to confirm first. Returns the deletedconnection_id. Requires the simulator:write OAuth scope. Connectors set up before this scope existed need to disconnect and reconnect to grant it; until then the tool returns error_category: "insufficient_scope".
Parameters:
Real connections can’t be deleted: a real, deleted or unknown connection returns a “not found” error.
Webhooks
Create and manage the webhooks that receive your events. These tools manage the same webhooks as the Webhooks page of the Subtotal Dashboard and the public webhooks API. Listing needs thewebhooks:read OAuth scope; creating, updating and deleting need webhooks:write. Connectors set up before these scopes existed need to disconnect and reconnect to grant them; until then the tools return error_category: "insufficient_scope".
Invalid input returns an error field with error_category: "invalid_input": an unknown webhook_id or event type, a name already used by another of your webhooks, a start date after the end date, or a URL that isn’t https on port 443 or doesn’t resolve only to public IP addresses.
list_webhooks
API: GET /webhooks List your webhooks with their subscribed events. The signing secret is never returned. Parameters: None Response format:create_webhook
API: POST /webhooks Create a webhook subscribed to the given events. It starts paused unlessis_active is true.
Parameters:
Response format: the webhook, in the
list_webhooks shape, plus signing_secret.
signing_secret verifies the X-Subtotal-Signature header on every delivery (see Verifying signatures). It’s shown only in this response and can’t be read back. If it’s lost, delete the webhook and create a new one.update_webhook
API: PATCH /webhooks/{webhook_id} Update a webhook. Every argument exceptwebhook_id is optional and omitted ones are left unchanged, but a call must change at least one field. event_types replaces the subscribed events.
Parameters:
Response format: the updated webhook, in the
list_webhooks shape.
This tool can set purchase date bounds but not clear them. Clear a bound with the Update webhook API. The signing secret doesn’t change.
delete_webhook
API: DELETE /webhooks/{webhook_id} Delete a webhook. It stops receiving events immediately, and this can’t be undone. To stop deliveries temporarily, pause it withupdate_webhook and is_active: false instead.
Parameters:
Response format: