Share a Wish

Wishlist API

The Share a Wish Public API is a plain JSON REST API (OpenAPI 3.1). This page covers authentication and the endpoints a shop integration needs: minting a widget token, saving products, reading what a shopper saved from your store, and managing hosted wishlists. Everything else is in the interactive reference and the OpenAPI YAML.

Base URL

https://nqwfhjycwrtukfszuofi.supabase.co/functions/v1/public-api

All paths on this page are relative to that URL. There is a single production environment; test keys (pk_test_…) talk to the same server and database. The spec notes that requests from outside Share a Wish's own domains must also carry the Supabase anon apikey header — the SDK sets it for you. Version: 1.1.0.

Authentication

Three token types, matching the securitySchemes in the spec:

TokenHeaderUsed for
Public keyAuthorization: Bearer pk_live_… or pk_test_…POST /widget/init, GET /api-usage/{apiKeyId}. Identifies the partner. Safe in browsers: it can only mint widget tokens for allow-listed origins.
Widget tokenX-Widget-Token: <jwt>All /widget/* endpoints after init. Signed JWT, 10-minute TTL, bound to the requesting origin.
User tokenAuthorization: Bearer <supabase access token>Everything that reads or writes an end user's lists: /widget/save, /widget/saved-products, /hosted/*, /me/*.

Keys and environments

Create keys in the Partner Portal under API & Integrations → API Keys. pk_test_… keys work on every plan, on localhost and staging domains. pk_live_… keys are for production; live keys created after 2026-09-01 must have at least one allowed domain (wildcards like *.example.com are supported), otherwise /widget/init answers 403 allowlist_required. An origin that is not on the list gets 403 origin_not_allowed.

POST /widget/init — mint a widget token

First call of every SDK session. Validates the public key (status active), checks the request origin against the key's domain allow-list and returns a signed widget token plus the partner's basket configuration. Auth: public key.

POST /widget/init
Authorization: Bearer pk_test_XXXXXXXXXXXXXXXX
Content-Type: application/json

{ "origin": "https://shop.example.com" }

origin (required) is window.location.origin of the embedding page. public_key may be sent in the body instead of the header.

HTTP/1.1 200 OK
X-Usage-Total: 1234
X-Usage-Limit: 15000
X-Usage-Remaining: 13766

{
  "token": "eyJhbGciOi…",
  "partner_id": 42,
  "api_key_id": 7,
  "exp_minutes": 10,
  "overage": false,
  "basketConfig": { "layout": "grid", "primaryColor": "#2563EB", "checkout": "ajax", … }
}

WidgetInitResponse: token (string, value for X-Widget-Token), partner_id (integer), api_key_id (integer or null), exp_minutes (integer, 10), overage (boolean, informational — the widget keeps working), basketConfig (object or null). The X-Usage-Total / X-Usage-Limit / X-Usage-Remaining headers on this response count the partner's wishlist actions of the current month against the plan limit (see rate limits & usage).

Errors: 400 invalid_input, 403 unknown_key, 403 origin_not_allowed, 403 allowlist_required, 403 subscription_required (Shopify shop whose trial expired; body contains trial_expired: true).

POST /widget/save — save a product

Upserts the product by canonical URL (tracking parameters stripped), stores localized title/description/image, uploads the image, records a price point and links the product to the wishlist. Auth: widget token and user token. The hosted save page does this for you; call it directly only if you build your own save UI on top of a Share a Wish user session.

POST /widget/save
X-Widget-Token: eyJhbGciOi…
Authorization: Bearer <supabase access token>
Content-Type: application/json

{
  "wishlistId": 1234,
  "product": {
    "url": "https://shop.example.com/products/42",
    "title": "Blue Sneaker",
    "description": "Lightweight everyday sneaker",
    "imageUrl": "https://shop.example.com/img/42.jpg",
    "price": 7990,
    "currency": "EUR",
    "marketCode": "de-DE",
    "offerId": 42,
    "metadata": { "variantId": "987", "quantity": 1 }
  }
}

SaveRequest: wishlistId (integer, required — the numeric db_id of a list, not the share token) and product (SaveProductInput, url required). price is an integer in minor units. partnerMeta / metadata (variant ids, cart parameters) are stored when siteDomain matches the widget origin.

{ "ok": true, "item_id": 98765, "product_id": 55521 }

Errors: 400 missing_wishlist, 400 missing_url, 401 unauthorized, 403 invalid_wishlist (list does not belong to the user), 500 product_create_failed (includes db_error), 500 save_failed.

GET /widget/saved-products — what the user saved from your shop

Returns the user's saved products filtered to the current partner (source_partner_id, fallback: the product's source_domain equals the widget origin). If the filter would return an empty set while the user has products, all products are returned as a fallback. Auth: widget token + user token. A POST variant with the same response exists for clients that cannot send custom headers on GET. DELETE /widget/saved-products/{itemId} removes an item saved from this domain.

GET /widget/saved-products
X-Widget-Token: eyJhbGciOi…
Authorization: Bearer <supabase access token>
{
  "products": [
    {
      "id": 55521,
      "wishlist_id": 1234,
      "item_id": 98765,
      "source_partner_id": 42,
      "source_shop_id": null,
      "source_domain": "shop.example.com",
      "title": "Blue Sneaker",
      "description": "Lightweight everyday sneaker",
      "imageUrl": "https://…/products/55521.jpg",
      "affiliateUrl": "https://shop.example.com/products/42",
      "url": "https://shop.example.com/products/42",
      "price_minor": 7990,
      "currency": "EUR",
      "compare_at_price_minor": null,
      "brand": null,
      "product_type": null,
      "tags": null,
      "checkoutMeta": { "variantId": "987", "quantity": 1 },
      "variantId": "987"
    }
  ]
}

SavedProduct fields: id, wishlist_id, item_id, source_partner_id, source_shop_id, source_domain, title, description, imageUrl / image_url, affiliateUrl, url, price_minor / price, currency, compare_at_price_minor, brand, product_type, tags, checkoutMeta, variantId. The response carries the X-RateLimit-* and X-Usage-* headers.

Hosted lists — CRUD

Hosted lists are the user's wishlists as shown on shareawish.de and in the app. All calls take a user token. Lists are addressed by their share token; the numeric db_id is what you pass as wishlistId to save calls. See Hosted Wishlists & Share Links for the product flow.

MethodPathPurpose
POST/hosted/listsLists owned by or shared (editor) with the user, sorted by updated_at desc.
POST/hosted/me/listsCreate a list. Body { "name": string, "description"?: string }. Returns { ok, list, token }.
GET/hosted/lists/{token}List metadata (owner or editor). Query: market, lang.
PUT/hosted/lists/{token}Update name (alias title), description, share_public, image_path, occasion_slug.
DELETE/hosted/lists/{token}Delete the list and its items (owner only).
GET/hosted/lists/{token}/itemsItems of the list as { products: HostedProduct[] }.
POST/hosted/saveSame contract as /widget/save, user token only; also accepts product.productId, rawProductId, sourceLabel, priority (1–4).
PUT/hosted/lists/{token}/items/{itemId}/purchasedToggle the purchased flag. Body { "purchased": boolean }.
POST /hosted/lists
Authorization: Bearer <supabase access token>

{
  "lists": [
    {
      "id": "k3Jd9…",             // share token → /hosted/lists/{token}
      "db_id": 1234,               // wishlistId for save calls
      "name": "Birthday",
      "description": "",
      "image_path": "",
      "share_public": true,
      "occasion_slug": "birthday",
      "productCount": 12,
      "collaboratorCount": 0,
      "is_shared": false,
      "role": "owner",
      "owner_id": "3f1c…",
      "owner_name": "Emma",
      "owner_avatar": null,
      "updated_at": "2026-09-01T10:12:00Z"
    }
  ]
}

HostedList fields: id (share token), db_id, name, description, image_path, share_public, occasion_slug, productCount, collaboratorCount, is_shared, role (owner | editor | viewer), owner_id, owner_name, owner_avatar, updated_at.

GET /hosted/public/lists/{token} — public share page data

No authentication. Only lists with share_public = true are returned; includes the owner's display name and avatar. GET /hosted/public/lists/{token}/items returns the products ({ products: HostedProduct[] } with purchased and added_at), and PUT /hosted/public/lists/{token}/items/{itemId}/purchased lets a gift-giver mark an item as bought without an account.

GET /hosted/public/lists/k3Jd9…?lang=en&market=de-DE

{
  "list": {
    "id": "k3Jd9…",
    "internal_id": 1234,
    "name": "Birthday",
    "description": "",
    "image_path": "",
    "share_public": true,
    "created_at": "2026-08-01T08:00:00Z",
    "updated_at": "2026-09-01T10:12:00Z",
    "occasion_slug": "birthday",
    "occasion_title": "Birthday",
    "productCount": 12,
    "owner_username": "emma",
    "owner_avatar": null,
    "owner_is_premium": false
  }
}

HostedProduct fields: id, wishlist_id, item_id, purchased, title, description, imageUrl, affiliateUrl, price_minor, currency, source_label, priority, rating, review_count, brand, added_at. Errors: 404 not_found for unknown or private tokens.

Errors

Every error is JSON { "error": "<snake_case_code>", "message"?: string } with a 4xx/5xx status; some responses add fields such as db_error or trial_expired. Handlers that catch unexpected exceptions return { "error": "Internal server error" } with status 500. The complete list is on the error codes page.

Download the machine-readable spec: /openapi/shareawish-public-api.yaml · endpoint summary: /openapi/summary.json