Wishlist API & Save Widget
Add a cross-device wishlist to any store in an afternoon. One script tag, one public key, and a REST API for everything else.
Guides
Save Button
Embed widget.js, data-shareawish attributes, init(), events, React and Shopify Liquid.
Wishlist API
Keys and tokens, /widget/init, /widget/save, saved products, hosted list CRUD with real payloads.
Hosted Wishlists & Share Links
Create lists, share by token, public view without account, mark as purchased, Open Graph previews.
Shopify Wishlist App
Install flow, the five theme app extension blocks and their settings, plans and the 14-day trial.
Rate Limits & Usage
Wishlist actions as the billing unit, per-plan limits, 80% / 100% notifications, X-RateLimit-* and X-Usage-* headers.
Error Codes
Every error code with HTTP status and meaning, from origin_not_allowed to ad_studio_disabled.
MCP Servers
Use Share a Wish from Claude, Cursor or Copilot: @shareawish/mcp ships a Creator Shop server and a Wishlist Integration server, installed via npx, authenticated with a personal access token.
Getting started in three steps
-
1. Create a test key
In the Partner Portal open API & Integrations → API Keys and create a key with environment Test. You get a
pk_test_…key. Test keys work on every plan, can be used onlocalhostand staging domains and hit the same live API as production keys — there is no separate sandbox, so the data you save is real.Production keys (
pk_live_…) need at least one allow-listed domain when created after 2026-09-01;POST /widget/initrejects other origins withorigin_not_allowed. -
2. Embed the Save Button
<script src="https://shareawish.shop/sdk/v1/widget.js" data-shareawish-key="pk_test_xxx" defer></script> <button data-shareawish data-url="https://shop.example.com/p/trail-shoes" data-title="Trail Running Shoes" data-image-url="https://shop.example.com/img/shoes.jpg" data-price="12999" data-currency="EUR"> Save to wishlist </button>Or programmatically:
ShareAWish.init({ key: 'pk_test_xxx' })andShareAWish.open({ productUrl, title, imageUrl, price, currency })from a click handler. The SDK callsPOST /widget/initwith your key and the page origin, receives a widget token (60 minutes) and opens the hosted save dialog athttps://shareawish.shop/save(URL carriestoken,key,origin). Details: Save Button guide.Canonical SDK URL:
https://shareawish.shop/sdk/v1/widget.js. The legacy/sdk/save-sdk.jsstays served but is deprecated (no redirect, security fixes only);cdn.shareawish.shop/sdk/v1/save-sdk.js,cdn.shareawish.com/widget.jsand thedata-saw-keyattribute are deprecated. -
3. Show the basket
The basket is a hosted page (
https://shareawish.shop/basket) that the SDK mounts in an iframe. It shows the products the shopper saved from your shop, handles sign-in and offers "Add to cart" / "View product". The modern default is a side panel (drawer) opened from a wishlist button in your header; the iframe loads on first open.<button type="button" id="wishlist-button">Wishlist</button> <script src="https://shareawish.shop/sdk/v1/basket-integration.js"></script> <script> var basket = ShareWishBasket.init({ apiKey: 'pk_test_xxx', configId: 'bkt_xxx', // from Partner Portal → Basket Integration (or baskets_create via MCP) onAddToCart: function (d) { // the shopper pressed "Add to cart" in the basket addToCart(d.variantId, d.quantity).then(function (ok) { d.respond(ok); }); } }); var drawer = basket.drawer({ trigger: '#wishlist-button', side: 'right', width: '440px', title: 'My wishlist' }); ShareAWish.on('saved', function () { drawer.refresh(); }); // keep it in sync with the save button </script>Sign-in once per device. Browsers partition storage inside third-party iframes, so the basket signs shoppers in through a small first-party window on shareawish.shop and adopts that session. A shopper who is already signed in there (from another shop or the save button) sees the window open and close; nobody has to sign in per shop. "View full wishlist" hands the session over to shareawish.de the same way.
Other placements: an inline section (
basket.renderInto('#sharewish-basket'), e.g. on an account or "My wishlist" page) or a standalone/wishlistpage with that section as its only content. Shopify shops use the app's theme block instead (no code).Configuration — layout (
grid,list,cards,compact), colours, font (inheritrecommended), border radius, hover animation, button labels, shop name and empty-state links, cart URL pattern and webhook URL — lives in the basket configuration (GET /baskets/{id}), so you can restyle without redeploying.How "Add to cart" reaches your shop — the hosted basket cannot touch your cart directly, so on click it tries in this order: (1)
webhookUrlset → it POSTs{ type: 'basket_checkout' | 'basket_bulk_checkout', product | products, meta, origin }to your server and stops; (2) the item carries a variant id → the SDK dispatchessharewish:basket.addToCart/…addToCartBulk(also available asonAddToCart/onAddToCartBulkinit options) withdetail.respond(ok, error)— call your platform's cart API (Shopify/cart/add.js, WooCommerce?wc-ajax=add_to_cart, your own endpoint) and confirm; (3) a cart URL pattern such ashttps://shop.example.com/cart/add?id={variantId}&quantity={quantity}is opened in a new tab; (4) otherwise the product/affiliate URL is opened ("View product"). The variant id comes from the save button:data-metadata='{"variantId":"123","quantity":1}'.Events on
window(prefixsharewish:, orbasket.on(name, fn)):basket.mounted,basket.drawer{ open },basket.addToCart,basket.addToCartBulk,basket.checkout,basket.error. The basket reads its configuration fromGET /baskets/{id}, tracks opens/checkouts viaPOST /baskets/{id}/eventand lists the shopper's products saved from your shop throughGET /widget/saved-products.The embedding page must be served over https (localhost excepted). The basket renders in an iframe from
https://shareawish.shop/basket, which is delivered withContent-Security-Policy: frame-ancestors 'self' https: http://localhost:* http://127.0.0.1:*—http://localhostworks for local development, any other plainhttp://page (LAN IP, http staging host) shows an empty iframe. The SDK detects this, logsinsecure_originand dispatchessharewish:basket.error.Errors are surfaced as a DOM event:
window.addEventListener('sharewish:basket.error', e => console.warn(e.detail.code))— codes:insecure_origin,config_required,key_required,config_not_found,mount_target_not_found, plus the/widget/initcodes (unknown_key,origin_not_allowed, …). See error codes.
Authentication at a glance
| Token | Header | Used for |
|---|---|---|
| Public key | Authorization: Bearer pk_live_… | pk_test_… | POST /widget/init, GET /api-usage/{id}. Safe in browsers. |
| Widget token | X-Widget-Token: <jwt> | All /widget/* calls after init. Bound to the page origin, 60 min TTL; expired → 401 widget_token_invalid, re-run init. |
| User token | Authorization: Bearer <supabase access token> | Everything that reads or writes a user's lists (/widget/save, /hosted/*, /me/*). |
Full details, request and response examples: Wishlist API.
Rate limits & usage
Plans are metered in wishlist actions per month — a save, a share or a hosted list view — from 200 on Free to 60,000 on Scale. Requests are never rejected for quota reasons (only the abuse protection on /widget/init, /widget/oauth-* and /baskets/{id}/event answers 429 with Retry-After); you are notified at 80% and 100% of the quota. Paid plans keep working above the quota and pay per additional action (Starter 2.5 ct, Growth 1.0 ct, Scale 0.5 ct); the Free plan is paused at 100% and POST /widget/save answers 402 usage_limit_reached. Responses on /widget/*, /hosted/*, /shops/* and the basket routes carry informational X-RateLimit-* and X-Usage-* headers. See Rate Limits & Usage.
Endpoint overview
Generated from the OpenAPI spec. Endpoints tagged consumer or internal serve the Share a Wish app and portal and are documented for completeness; the partner-facing surface is Widget, Hosted, Baskets, Shops, Discover and Stores.
Loading /openapi/summary.json…
Errors
Errors are JSON with a machine-readable code: { "error": "origin_not_allowed" }, optionally with message. Common codes: invalid_input (400), unauthorized (401), origin_not_allowed / unknown_key / allowlist_required / subscription_required (403 on /widget/init), invalid_wishlist (403), not_found (404), product_create_failed (500, includes db_error). Complete list: Error Codes.