StoreConnect API
On this page
:::note This article documents the commerce API: the system-to-system REST surface for creating customers and carts, taking payment, and reading orders and products, authenticated with the store’s API key. To build or manage a store with an AI agent instead, see the end-to-end store build sequence and how to connect an agent to your store. :::
The StoreConnect API lets external applications run a complete commerce flow against a store without using the StoreConnect storefront: create customers, build carts, take payment, save cards, start and manage subscriptions, and read orders and products. Everything a customer does on your website, your own application can do through this API.
Typical uses include SaaS billing (your application signs customers up to subscription plans and StoreConnect handles payment, invoicing, and renewals), headless storefronts, and back-office integrations.
Authentication and base URL
Every request sends a bearer token made up of your store’s Salesforce ID and your API key, separated by a colon:
```
Authorization: Bearer STORE_ID:API_KEY ```
The API is available at two equivalent base URLs:
| Form | Example |
|---|---|
| Subdomain | https://api.yourstore.com/v1 |
| Path | https://yourstore.com/api/v1 |
Before the examples below will run, the store needs an api subdomain, a store record pointed at it, and an API key. See API configuration for that setup.
The API key is read from the api.auth_key store variable on your Store record in Salesforce, falling back to a key issued by Support for the store. Path-based access can be turned off with the api.disable_path_access store variable. See Store variables for how to set store variables.
Requests without a valid token receive 401 Unauthorized.
:::note
Your store’s live OpenAPI specification is served at https://api.yourstore.com/v1/api.yml, and a browsable Swagger UI at https://api.yourstore.com/. The specification is always current for the version your store runs.
:::
Concepts
The API works with the same records your store and Salesforce org use:
| Record | What it is |
|---|---|
| Contact | A person: name, email, and phone. Belongs to an account. |
| Account | The customer record that holds billing and shipping addresses and syncs to Salesforce. |
| Cart | A basket of items being prepared for checkout. |
| Order | The result of a successful checkout. Syncs to Salesforce. |
| Subscription | Created automatically when an order contains a subscription product. Renewals generate new orders on schedule. |
| Payment method | A card saved against a contact at the payment gateway for recurring or later charges. |
Record IDs returned by the API (the id field) are StoreConnect IDs, with one exception: payment providers in the GET /store response return their Salesforce ID. Endpoints that take an ID in the path accept either the StoreConnect ID or the Salesforce ID of the same record. IDs sent in a request body are not always interchangeable, so the sections below say which form each one takes.
List endpoints (/products, /orders, /subscriptions, /payment_methods) accept page and per_page query parameters and return X-Pagination-Current-Page, X-Pagination-Per-Page, and X-Pagination-Total-Records response headers.
Store and products
GET /store returns the store’s configuration, including the payment providers your application charges against:
```json
{ “name”: “Your Store”, “currency”: { “code”: “USD”, “symbol”: “$” }, “locale”: “en-US”, “logo”: { “id”: “…”, “sizes”: { “original”: “…” } }, “timezone”: “America/Los_Angeles”, “payment_providers”: [ { “id”: “PROVIDER_ID”, “name”: “Stripe”, “method”: “stripe”, “publishable_key”: “pk_test_…” } ] } ```
Only active providers are listed. A provider’s id is its Salesforce ID, and this is the value you pass as provider_id at checkout and when saving a card. publishable_key is the provider’s client-side key for tokenizing cards in the browser; it is null for gateways that have no client-side key. Secret keys are never exposed.
GET /products returns the paginated catalog and GET /products/{id} returns one product with extended detail (content sections, variants, and related products). The fields you need most:
| Field | Description |
|---|---|
id, code, name, summary |
Identity and display fields. |
pricing.current, pricing.original, pricing.tax_inclusive |
Current pricing. |
pricing.subscription |
Present for subscription products: term, term count and unit, and total price. Empty for one-off products. |
stock |
Available-to-sell totals and per-location stock levels. |
images, categories, tags, taxes |
Catalog metadata. |
When you sell subscription plans, each plan is a product whose pricing.subscription describes the billing term.
Customers: contacts and accounts
Create a contact
POST /contacts finds or creates a contact by email, and finds or creates its account:
```json
{ “first_name”: “Alex”, “last_name”: “Taylor”, “email”: “alex@example.com”, “phone”: “+1 555 0100”, “company_name”: “Example Co”, “external_reference”: “your-system-id-123”, “billing_address”: { “street”: “123 Example Street”, “city”: “San Francisco”, “state”: “CA”, “country”: “US”, “postal_code”: “94105” } } ```
first_name,last_name, andemailidentify the contact. If a contact with the email already exists it is reused.- The account name comes from
company_namewhen given, otherwise from the contact’s name. external_referencestores your own system’s identifier on the contact and is returned in every contact response.countryis the two-letter ISO code.stateis required for countries with state or province lists (for example US, AU, CA).
The response returns the contact and its account_id:
```json
{ “id”: “…”, “first_name”: “Alex”, “last_name”: “Taylor”, “email”: “alex@example.com”, “phone”: “+1 555 0100”, “account_id”: “…”, “external_reference”: “your-system-id-123”, “custom_data”: {} } ```
GET /contacts/{id} accepts a StoreConnect ID, a Salesforce ID, or an email address. PATCH /contacts/{id} updates first_name, last_name, email, and phone.
Read and update an account
GET /accounts/{id} returns the account with both addresses:
```json
{ “id”: “…”, “name”: “Example Co”, “display_name”: “Example Co”, “tax_entity_code”: null, “billing_address”: { “street”: “…”, “city”: “…”, “state”: “…”, “country”: “…”, “postal_code”: “…” }, “shipping_address”: { “street”: “…”, “city”: “…”, “state”: “…”, “country”: “…”, “postal_code”: “…” }, “custom_data”: {} } ```
PATCH /accounts/{id} updates name, tax_entity_code, billing_address, and shipping_address. Address updates are partial, so only the fields you send change:
```json
{ “shipping_address”: { “street”: “500 Delivery Lane”, “city”: “Oakland”, “state”: “CA”, “country”: “US”, “postal_code”: “94607” } } ```
How billing and shipping addresses work
Addresses live in two places, and they serve different purposes:
| Where | Purpose |
|---|---|
| Account | The customer record in Salesforce, used for tax, invoicing, and CRM. |
| Cart | Captured at checkout and copied onto the order. Subscription renewal orders reuse the original order’s addresses. |
Points to be aware of:
- When
POST /contactscreates an account, the billing address you supply is also copied into the account’s shipping address. To store a different shipping address, follow up withPATCH /accounts/{id}. - The cart’s shipping address, not the account’s, is what appears on the order and on every renewal order for a subscription started from that cart. Set it on the cart at checkout time if it matters for fulfillment.
- A cart’s
billing_addressis required and must be complete (all five fields). Itsshipping_addressis optional for digital goods, and required when the store has shipping enabled and the cart contains a physical product.
Carts and checkout
Create a cart
POST /carts:
```json
{ “contact_id”: “CONTACT_ID”, “items”: [ { “product_id”: “PRODUCT_ID”, “quantity”: 1 } ], “billing_address”: { “street”: “…”, “city”: “…”, “state”: “…”, “country”: “…”, “postal_code”: “…” }, “shipping_address”: { “street”: “…”, “city”: “…”, “state”: “…”, “country”: “…”, “postal_code”: “…” } } ```
The response includes priced items and the checkout mode:
```json
{ “id”: “…”, “status”: “shopping”, “contact_id”: “…”, “account_id”: “…”, “items”: [ { “product_id”: “…”, “product_name”: “Pro Plan”, “quantity”: 1, “unit_price”: 29.0, “has_trial”: false, “trial_days”: 0 } ], “total_amount”: 29.0, “total_payable”: 29.0, “checkout_mode”: “charge_and_save”, “billing_address”: { “…”: “…” }, “shipping_address”: { “…”: “…” }, “order_id”: null, “custom_data”: {} } ```
checkout_mode tells your application what the checkout call will do with the card:
| Mode | Meaning |
|---|---|
charge |
One-off items only. The card is charged and not saved. |
charge_and_save |
The cart contains a subscription. The card is charged now and saved for renewals. |
save_card_only |
The cart contains a subscription with a trial (billing delay). The card is saved and nothing is charged now. |
PATCH /carts/{id} updates items (replaces the item list), billing_address, shipping_address, contact_id, and abandoned. GET /carts/{id} returns the cart, and DELETE /carts/{id} removes it.
Check out
POST /carts/{id}/checkout completes the purchase:
```json
{ “payment”: { “token”: “PAYMENT_TOKEN”, “method”: “stripe”, “provider_id”: “PROVIDER_ID” } } ```
tokenis a one-time card token from your payment gateway’s client-side tokenization (for example Stripe Elements). The API never accepts raw card numbers.methodandprovider_ididentify the gateway; both come from thepayment_providerslist in theGET /storeresponse.
Depending on checkout_mode, checkout saves the card, charges it, or both, then creates the order. The response includes the order and, when the cart contained a subscription product, the subscription that was started.
Checkout is idempotent per cart: if the same cart is checked out twice (for example a retry after a timeout), the second call returns the already-created order rather than charging again.
The checkout response includes everything that was created:
```json
{ “order”: { “id”: “…”, “reference”: “…”, “status”: “…”, “total_amount”: 29.0, “total_paid”: 29.0, “items”: [ { “…”: “…” } ] }, “payment_method”: { “id”: “…”, “card_brand”: “visa”, “last_four”: “4242”, “is_default”: true }, “payment”: { “status”: “…”, “amount”: 29.0 }, “subscription”: { “id”: “…”, “status”: “active”, “next_billing_date”: “…” } } ```
paymentis present when a charge was made (chargeandcharge_and_savemodes).payment_methodis present when the card was saved (charge_and_saveandsave_card_onlymodes).subscriptionis present when the cart contained a subscription product.
Payment methods and saved cards
Checking out a cart that contains a subscription saves the card automatically. These endpoints let you manage saved cards outside a purchase.
GET /payment_methods?contact_id={id} lists a contact’s active saved cards. contact_id is required, not a filter: a request without it returns 404.
```json
[ { “id”: “…”, “type”: “card”, “card_brand”: “visa”, “last_four”: “4242”, “expires_at”: “…”, “exp_month”: 12, “exp_year”: 2030, “is_default”: true, “status”: “active”, “display_name”: “visa ending in 4242”, “created_at”: “…”, “custom_data”: {} } ] ```
POST /payment_methods vaults a new card from a gateway token and makes it the contact’s default. Use it when a customer replaces their card:
```json
{ “contact_id”: “CONTACT_ID”, “payment”: { “token”: “PAYMENT_TOKEN”, “provider_id”: “PROVIDER_ID” } } ```
DELETE /payment_methods/{id} deactivates a saved card and returns 204 No Content. The record is not deleted, so it stops appearing in the list above but any history that references it is preserved.
Collecting card details without handling them
POST /payment_method_sessions with a contact_id returns a short-lived URL you can embed in an iframe:
```json
{ “token”: “…”, “expires_in”: 1800, “path”: “/embedded_payment/save_card?token=…” } ```
Open https://yourstore.com{path} in an iframe and the customer enters their card on a StoreConnect-hosted form; the card is vaulted against the contact without any purchase. The token is valid for 30 minutes and works for that contact only. This is the recommended way to collect card details in production, because raw card data never touches your application.
Subscriptions
A subscription is created automatically when a checked-out cart contains a subscription product. Renewal orders and renewal charges then happen on schedule inside StoreConnect. See Subscription renewal orders.
GET /subscriptions?contact_id={id} lists a contact’s subscriptions, and GET /subscriptions/{id} returns one. Key fields:
| Field | Description |
|---|---|
status |
active, cancelled, expired, suspended, or delinquent. |
type |
evergreen or fixed. |
product_id, product_name, period_price, period_length, period_type |
The plan and its billing term. |
start_date, end_date, next_billing_date, next_renewal_date, trial_ends_at |
Lifecycle dates. |
pending_plan_change |
A scheduled downgrade that has not taken effect yet, or null. |
last_plan_change |
The most recent completed plan change, with direction and proration amount. |
payment_source |
Masked identifier and expiry of the card that renewals charge. |
GET /subscriptions/{id}/payment_method returns the card behind the subscription: { "subscription_id": "…", "payment_provider": "…", "identifier": "…", "expires_at": "…" }.
Change plan
PATCH /subscriptions/{id} with a product_id moves the subscription to another plan:
```json
{ “product_id”: “NEW_PLAN_PRODUCT_ID” } ```
StoreConnect decides the direction and timing from the price difference, so you do not pass them:
- A more expensive plan is an upgrade and takes effect immediately, with proration according to the store’s plan change settings.
- A cheaper plan is a downgrade and takes effect at the next billing date. Until then it appears in
pending_plan_change.
Plan changes apply to evergreen subscriptions only. When the change is not possible the endpoint returns 422 with type invalid and a reason such as not_evergreen, subscription_not_active, same_price, or payment_method_required. A PATCH with only custom_data merges your data without changing the plan.
Cancel
DELETE /subscriptions/{id} cancels an evergreen subscription and returns it with status: "cancelled". Subscriptions that are fixed term or already ended return 422 with type invalid and reason not_cancellable.
Orders, payments, and refunds
GET /orders?contact_id={id} lists orders and GET /orders/{id} returns one:
```json
{ “id”: “…”, “reference”: “…”, “status”: “…”, “total_amount”: 29.0, “total_tax”: 2.9, “total_paid”: 29.0, “total_payable”: 0.0, “customer”: { “id”: “…”, “first_name”: “…”, “last_name”: “…”, “email”: “…”, “phone”: “…” }, “customer_notes”: null, “items”: [ { “id”: “…”, “name”: “…”, “quantity”: 1, “price”: 29.0, “total”: 29.0, “tax”: 2.9 } ], “shipping_address”: { “…”: “…” }, “custom_data”: {} } ```
Orders collected at a store location return a collection_point_id instead of shipping_address.
POST /orders creates an order directly in a single call: customer, addresses, and items together, with optional collection_point_id or shipping_rate_id (one or the other), customer_notes, and finalise_order. Use it when payment happens outside StoreConnect; for card payments through the API, prefer the carts and checkout flow above.
POST /payments records a payment taken outside StoreConnect against an order. It does not charge a card:
```json
{ “order_id”: “ORDER_ID”, “amount”: 29.0, “reference”: “your-receipt-ref”, “source”: “External POS” } ```
All four fields are required, and amount must be greater than zero. Unlike IDs in a path, order_id here must be the StoreConnect ID; a Salesforce ID returns 404. GET /payments/{id} returns the payment with its status, amounts, and per-item allocations.
Refunds
Three endpoints drive refunds:
GET /orders/{order_id}/refunds/optionslists the payments that can be refunded, with the remaining refundable credit per payment.GET /orders/{order_id}/refunds/itemslists the order items that can be refunded, with quantities and refundable amounts.POST /orders/{order_id}/refundsrequests the refund:
```json
{ “salesforce_user_id”: “005…”, “reason”: “Customer request”, “requires_approval”: false, “refund_items”: [ { “payment_id”: “…”, “order_item_id”: “…”, “quantity”: 1, “amount”: -29.0 } ] } ```
Refund amounts are negative. With requires_approval: false the refund is processed immediately; otherwise it waits for approval in Salesforce. The response returns the created refund payment IDs.
Shipping rate estimates
POST /shipping_rate_estimates quotes shipping before checkout. Send either a cart_id (the cart’s items and shipping address are used) or an ad hoc list:
```json
{ “items”: [ { “product_id”: “PRODUCT_ID”, “quantity”: 2 } ], “address”: { “street”: “…”, “city”: “…”, “state”: “…”, “country”: “…”, “postal_code”: “…” } } ```
The response groups rates by delivery group:
```json
{ “delivery_groups”: [ { “key”: “…”, “shipping_methods”: “…”, “rates”: [ { “type”: “provider”, “id”: “…”, “name”: “Express”, “price_excluding_tax”: “12.5”, “provider”: “…” } ] } ] } ```
Two optional extras: send outlet_id to quote against a specific outlet rather than the request’s default, and note that a request carrying more than 100 items is rejected with 422, type request, and reason too_many_items.
:::note
price_excluding_tax is returned as a decimal string, unlike order and payment amounts, which are JSON numbers.
:::
Custom data
POST and PATCH requests on contacts, accounts, carts, and checkout accept a custom_data object of your own keys. Values merge into any existing custom data rather than replacing it, and come back in every response for the record. Use it to hold references your application needs, such as workspace or tenant IDs.
Error handling
Errors return a consistent envelope:
```json
{ “error”: { “type”: “not_found”, “details”: {} } } ```
| Status | When |
|---|---|
401 |
Missing or invalid bearer token. Body is {"error": "Unauthorized"}. |
404 |
Record not found, with type set to not_found. |
422 |
Validation or processing failure, where type describes the failure, with the reason in details. |
The type values you will see on a 422 are:
| Type | When |
|---|---|
invalid |
A request that was understood but cannot be applied, such as a plan change to a product at the same price, or a payment for zero. This is the most common 422. |
request |
A malformed or incomplete request, such as a cart with no billing address or a shipping estimate with neither a cart_id nor items and an address. |
invalid_account, invalid_contact |
The account or contact failed validation on create or update, with the field errors in details. |
payment_failed, order_failed |
The gateway declined the charge, or the order could not be created after payment. |
For example, creating a cart without a complete billing address returns:
```json
{ “error”: { “type”: “request”, “details”: { “reason”: “billing_address_required” } } } ```
Live test: a minimal end-to-end flow with curl
This walkthrough runs the whole flow (customer, cart, checkout, subscription) from a terminal.
Before you start
Confirm all four of these, or the walkthrough fails partway through:
- A test store with the API set up, following API configuration.
- The store’s Salesforce ID and its API key.
- An active payment provider on that store, in test mode. Stripe is used below.
- At least one purchasable product. Use a subscription product to exercise steps 6 and 7 in full.
:::warning
Run this against a test store only. The card token below (tok_visa) is a Stripe test-mode token; in production your client-side code generates the token, so card details never touch your servers.
:::
Set your credentials once:
```bash
export API_BASE=”https://api.yourstore.com/v1” export AUTH=”Authorization: Bearer STORE_ID:API_KEY” ```
-
Confirm authentication and find your payment provider:
```bash
curl -s “$API_BASE/store” -H “$AUTH” ```
Note the
idandmethodof the provider you will charge against in thepayment_providersarray. -
Pick a product to sell:
```bash
curl -s “$API_BASE/products” -H “$AUTH” ```
Note the product
idof a subscription plan or product. -
Create the customer:
```bash
curl -s -X POST “$API_BASE/contacts” \ -H “$AUTH” -H “Content-Type: application/json” \ -d ‘{ “first_name”: “Alex”, “last_name”: “Taylor”, “email”: “alex@example.com”, “billing_address”: { “street”: “123 Example Street”, “city”: “San Francisco”, “state”: “CA”, “country”: “US”, “postal_code”: “94105” } }’ ```
Note
id(the contact) andaccount_idin the response. -
(Optional) Set a shipping address on the account, so it differs from the billing address copied across in step 3:
```bash
curl -s -X PATCH “$API_BASE/accounts/ACCOUNT_ID” \ -H “$AUTH” -H “Content-Type: application/json” \ -d ‘{ “shipping_address”: { “street”: “500 Delivery Lane”, “city”: “Oakland”, “state”: “CA”, “country”: “US”, “postal_code”: “94607” } }’ ```
-
Create the cart:
```bash
curl -s -X POST “$API_BASE/carts” \ -H “$AUTH” -H “Content-Type: application/json” \ -d ‘{ “contact_id”: “CONTACT_ID”, “items”: [{ “product_id”: “PRODUCT_ID”, “quantity”: 1 }], “billing_address”: { “street”: “123 Example Street”, “city”: “San Francisco”, “state”: “CA”, “country”: “US”, “postal_code”: “94105” } }’ ```
Note the cart
idand itscheckout_mode. -
Check out with a test card token:
```bash
curl -s -X POST “$API_BASE/carts/CART_ID/checkout” \ -H “$AUTH” -H “Content-Type: application/json” \ -d ‘{ “payment”: { “token”: “tok_visa”, “method”: “stripe”, “provider_id”: “PROVIDER_ID” } }’ ```
The response confirms the order and, for subscription products, the new subscription.
-
Verify the results. The checkout response already contains the order and subscription; confirm they read back correctly:
```bash
curl -s “$API_BASE/orders/ORDER_ID” -H “$AUTH” curl -s “$API_BASE/subscriptions?contact_id=CONTACT_ID” -H “$AUTH” curl -s “$API_BASE/payment_methods?contact_id=CONTACT_ID” -H “$AUTH” ```
The order shows as paid (
total_payableis0.0), the subscription isactivewith anext_billing_date, and, for subscription carts, the card is saved against the contact.
If every step returns the expected response, the store is configured correctly end to end: authentication, product catalog, customer creation, payment gateway, and order creation are all working.
Was this article helpful?
Thanks for your feedback! It helps us improve our docs.