Skip to main content
One signed-in shopper moves through the three surfaces that surround a purchase: they save the Watch to their wishlist and later move it into the cart, they review the product they bought, and they open a support thread about the order. Along the way come the atomic wishlist-to-cart move, a second review meeting a 409, a deleted message, and the two realtime connection shapes. Every call here acts on a signed-in shopper’s own data, so it pairs an API key with the shopper’s session token in xAuthToken. Workflow Examples has the client setup, and Storefront Checkout covers minting that session token. One shopper — Jane Doe, customer 9489529e-69b0-4e84-bea7-62c499085bd9 — and one product, the Samsung Galaxy Watch 6 (0186665f-…, variant e98631a9-…), carry throughout.
Wishlist, review, and messaging writes are browser-origin storefront writes, so the publishable client covers them; the shopper’s xAuthToken is what proves ownership. Moderating reviews and replying from the store side happen in the merchant admin and are not API actions.

Wishlist

The shopper is not ready to buy, and saves the Watch for later. Wishlists are always customer-scoped, so every call carries customer_id and the matching xAuthToken.

1. Adding to the wishlist

addToWishlist saves a variant and returns the refreshed wishlist (201 Created). Its items carry product_price, the price quoted at the time of saving — a field the cart’s items do not have.
Response
A variant can be saved once per customer. Adding the same variant again returns 409 Conflict ({ "error": { "code": "conflict", "message": "Item already exists in wishlist" } }) rather than creating a duplicate, which makes the 409 itself a reliable “already saved” signal.

2. Reading the wishlist

getWishlist lists the customer’s saved items in the same item shape as the add response. customer_id is required, as anonymous wishlists are not supported.
Response

3. Moving a saved item to the cart (atomic)

The shopper decides to buy. moveWishlistToCart is one atomic step: it validates stock, adds the item to the cart or merges it with an existing line, removes it from the wishlist, and returns the refreshed cart. The response echoes removed_wishlist_item_id to identify the saved item that is gone, and the returned cart item carries a new id, being a cart row rather than the wishlist row.
Response

4. Removing a saved item

A shopper discarding a saved item instead reaches removeFromWishlist, which deletes it by id. The customer_id query param is required, which is what scopes the delete to its owner.
Response
The wishlist sits alongside the cart in the same cartWishlist service. From here the Watch goes into the storefront checkout and is paid for, and the shopper returns to review it.

Reviews

The Watch has arrived and the shopper wants to review it. Reviews start hidden and become public only once the merchant approves them.

5. Submitting a review (starts pending)

submitReview creates the review with status: "pending", and it appears in public listings only after the merchant approves it in their admin. An order_id belonging to the shopper flags the review verified_purchase: true.
Response

6. The duplicate review → 409 Conflict

A shopper holds only one non-rejected review per product, so a second review from Jane for the same Watch returns 409. The resolution is editing the existing review rather than retrying the submit.
Response (409)

7. Listing approved reviews (public, with summary)

Once the merchant approves the first review, listReviews backs the product page’s review section. On a publishable key it returns only approved reviews and always includes a summary block — average rating plus star distribution. This is public catalog data, so no customer token is involved.
Response
rating and sort (newest / oldest / highest / lowest) filter and order the list, and the cursor from pagination.next_cursor pages it. storefront.reviews.getReview({ id }) fetches one review by id.

8. Marking a review helpful

Upvotes let a product page surface the reviews shoppers found useful. Only approved reviews accept votes, and there is no built-in de-duplication, so per-shopper voting state belongs in your own UI.
Response
A shopper can remove their own review with storefront.reviews.deleteReview({ id, xAuthToken: session.access_token }) — it returns { "success": true, "message": "Review deleted" }. Deleting someone else’s review returns 403.

Messaging

The shopper opens a support thread about the order, and it stays live so the store’s replies arrive without polling.

9. Starting a conversation

createConversation opens a thread and posts the shopper’s first message in one call. thread_type classifies the inquiry (general, order_inquiry, product_inquiry, support, complaint, delivery, return, refund, technical). A customer_id comes with the matching xAuthToken.
Response

10. Sending a follow-up message

sendMessage appends to the thread just opened, with the text in message. The token must own the thread; otherwise the call returns 403.
Response

11. Listing threads and reading the conversation

listThreads backs the shopper’s inbox, filtered by customer_id, with pinned threads first; getThreadMessages loads one conversation’s messages, oldest-first by default.
Response
Response

12. Editing or deleting a message

A shopper can revise or retract a message they sent. Both calls require a secret key and so run server-side. editMessage rewrites the content and stamps is_edited: true plus edited_at; deleteMessage soft-deletes — the row stays, marked is_deleted and excluded from the default message list — and returns the deleted_at timestamp.
Response
Response

13. Marking the thread read

Opening the conversation clears the shopper’s unread state.
Response

14. Streaming replies in real time

The subscribeToThread helper opens a WebSocket to the thread, delivering the store’s replies as they are sent rather than on a polling interval. The same call covers a shopper who signed in through Galactic Core and one who signed in through your own identity provider, and it pulls in no extra packages: it uses the platform WebSocket, available in browsers, Deno, Bun, and Node 22+.
For a shopper authenticated with your own identity provider, the signed assertion goes in externalAuth in place of authToken; everything else is identical:
The connection is authorized server-side: the credential is validated and confirmed to be the thread’s participant before the socket is accepted, so a shopper only ever receives their own thread’s messages.
A connection managed directly, without the helper, is a WebSocket to GET /v1/messaging/threads/{id}/subscribe carrying ?api_key= and either ?auth_token= or ?external_auth= in the URL, since browsers cannot set request headers on a WebSocket. The server pushes one JSON frame per new message: { "type": "message.created", "payload": { "thread_id", "message" } }. The text frame ping keeps an idle connection alive, and the server replies pong.Customer authentication covers bringing your own identity provider.

What’s next

  • The shopper arrived from a storefront checkout, which the wishlist, reviews, and support thread sit around.
  • A returning shopper can track orders and lodge a return, often opening a return-type thread from the same inbox.
  • Search & Discovery captures the add_to_wishlist event that put the Watch on the wishlist to begin with.
  • Wishlist, reviews, and messaging carry over to a multi-merchant storefront — see the Marketplace split checkout and unified shopper profile.