Error Codes
Every error response of the Share a Wish Public API is JSON with a machine-readable error code and an HTTP status. Some responses add a human-readable message, a hint, a db_error string or flags such as trial_expired. Match on the code, not on the message.
HTTP/1.1 403 Forbidden
Content-Type: application/json
{ "error": "origin_not_allowed" }
Unexpected exceptions are caught per handler and answered with { "error": "Internal server error" } and status 500 (a few older consumer routes use similar plain-text messages such as Not found or Product not found). The tables below list every snake_case code found in the API source, grouped by the area that emits it.
Widget — /widget/init, /widget/save, /widget/saved-products, OAuth bridge
| Code | Status | Meaning |
|---|---|---|
invalid_input | 400 | Missing public_key / origin on init, or missing URL on prefetch. |
unknown_key | 403 | The public key does not exist or is not active. |
origin_not_allowed | 403 | The request origin is not on the key's domain allow-list (localhost is always allowed). |
allowlist_required | 403 | A pk_live_ key created after 2026-09-01 has no allowed domain yet. Response includes a hint. |
subscription_required | 403 | Linked Shopify shop has an expired trial and no active subscription. Body contains message and trial_expired: true. |
usage_limit_reached | 402 | /widget/save: the partner is on the Free plan and has used its 200 wishlist actions for the calendar month — saving is paused until the next month or an upgrade. Body: message, plan: "free", actions_limit, upgrade_url. Show a friendly "wishlist paused" notice, do not retry. Paid plans never get this error (they pay per extra action, see Overage). |
forbidden | 403 | Widget token missing or invalid on a token-protected widget route. |
unauthorized | 401 | User token missing or invalid. |
missing_wishlist | 400 | wishlistId absent in the save request. |
missing_url | 400 | product.url absent in the save request. |
invalid_wishlist | 403 | The wishlist does not belong to the authenticated user. |
product_create_failed | 500 | The product could not be created or found; db_error carries the database message. |
save_failed | 500 | The wishlist item could not be written. |
missing_state | 400 | OAuth bridge: state parameter missing. |
invalid_session_data | 400 | OAuth bridge: session payload malformed. |
invalid_or_expired_state | 400 | OAuth bridge: state unknown or expired. |
invalid_session | 400 | Session store: access or refresh token missing. |
invalid_access_token | 401 | Session store: the supplied access token is not valid. |
missing_sid | 400 | Session store/restore: session id missing. |
internal_error | 500 | Unexpected failure in the OAuth / session routes. |
SDK client-side codes (error event, WidgetError.code)
| Code | Meaning |
|---|---|
popup_blocked | The browser blocked the popup — call open() from a user gesture. |
init_no_token | /widget/init answered without a token. |
key_required | init() was called without a key. |
not_initialised | open() / mount() called before init(). |
mount_target_not_found | The selector passed to mount() matched no element. |
Server codes from /widget/init are passed through unchanged (err.code), with err.trial_expired set for subscription_required.
Hosted lists — /hosted/*
| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | User token missing or invalid. |
forbidden | 403 | The user is neither owner nor editor of the list, or not the owner where required. |
not_found | 404 | List, item or product not found — also for private lists on the public routes. |
missing_token | 400 | Share token path parameter missing. |
missing_id | 400 | Product id missing (DELETE /hosted/products/{productId}). |
missing_item_id | 400 | Item id missing. |
missing_params | 400 | Required path/body parameters missing (items, move, rescrape, collaborators). |
missing_name | 400 | name missing when creating a list. |
missing_title | 400 | title missing when adding a manual item. |
missing_wishlist / missing_url | 400 | Save request incomplete. |
invalid_wishlist | 403 | The wishlist does not belong to the user. |
product_create_failed | 500 | Product upsert failed; db_error included. |
save_failed | 500 | Wishlist item could not be written. |
update_failed | 500 | Purchased flag / item update failed. |
override_delete_failed / item_delete_failed | 400 | Deleting the item's price override or the item itself failed. |
item_not_deleted | 500 | Item still exists after delete. |
missing_target / same_list | 400 | Move: target list missing or identical to the source. |
move_requires_owner | 403 | Only the owner may move items between lists. |
insert_failed / source_delete_failed | 500 | Move: insert into target or delete from source failed. |
no_canonical_url | 400 | Rescrape: the product has no URL to fetch. |
manual_product | 400 | Rescrape: manual items cannot be scraped. |
missing_user / self_invite | 400 | Collaborators: user id missing or inviting yourself. |
not_a_friend | 403 | Collaborators can only be added from the owner's friends. |
upsert_failed | 500 | Collaborator write failed. |
link_failed / unlink_failed / internal_select_failed | 500 | Following / unfollowing a public list failed. |
missing_data | 400 | Account bootstrap / migration body incomplete. |
missing_identifier | 400 | Legacy login: identifier missing. |
no_legacy_user | 404 | Legacy login: no matching user. |
auth_failed / invalid_auth_response / db_insert_failed / db_error / user_init_failed / flag_update_failed | 500 | Anonymous user creation or legacy login failed. |
old_user_not_found | 404 | Migration: the anonymous user does not exist. |
old_user_not_anonymous | 400 | Migration: the source account is not anonymous. |
migration_failed / internal_error | 500 | Migration failed. |
Infrastructure, keys and usage — /api-usage/{apiKeyId}, /me/usage
| Code | Status | Meaning |
|---|---|---|
invalid_api_key_id | 400 | apiKeyId is not a number. |
unauthorized | 401 | No bearer key, or the key does not match the requested id. |
api_key_not_found | 404 | No key with that id. |
plan_limits_not_found | 500 | The plan limits for the key's plan could not be loaded. |
internal_server_error | 500 | Unexpected failure in the usage / image proxy routes. |
partner_not_found | 404 | /me/usage: the portal user has no partner. |
not_found | 404 | Unknown route (catch-all). |
Baskets, shops, partners, analytics
| Code | Status | Meaning |
|---|---|---|
not_found | 404 | /baskets/{id}: basket configuration unknown; /partners/*: partner not found. |
shop_not_found | 404 | Creator shop unknown or not published. |
invalid_partner_id | 400 | /partners/{id}: id is not numeric. |
no_partner | 404 / 403 | The user is not mapped to a partner (404 on /partners, 403 on media management). |
no_linked_shop | 400 | Storefront listing requires a linked Shopify shop. |
forbidden | 403 | Not the partner owner / not an allowed analytics origin. |
partner_required | 400 | Analytics: partner id missing. |
forbidden_key_not_owned / forbidden_key_mismatch | 403 | Analytics: the API key belongs to another partner. |
api_key_not_found | 404 | Analytics: key unknown. |
invalid_payload | 400 | /analytics/track: body malformed. |
Media, images, scraper
| Code | Status | Meaning |
|---|---|---|
invalid_product_id | 400 | Product id missing or not numeric. |
product_not_found | 404 | Product unknown. |
invalid_shop_id | 400 | Shop id malformed. |
unsupported_mime / invalid_bytes | 400 | Upload: file type not allowed or size not numeric. |
file_too_large | 413 | Upload exceeds the size limit. |
plan_limit_reached | 402 | Video quota of the creator plan reached. |
invalid_storage_path / invalid_poster_path | 400 | Upload finalisation: storage paths malformed. |
object_not_found | 409 | Upload finalisation: the object is not in storage yet. |
invalid_id / invalid_position / invalid_status / nothing_to_update | 400 | Media update body invalid. |
media_rejected | 409 | A rejected video cannot be re-activated. |
not_found | 404 | Media item unknown. |
invalid_or_disallowed_url | 400 | /images/proxy: URL missing or host not allowed. |
upstream_error | upstream status | /images/proxy: the origin server answered with an error; its status code is passed through and echoed as status. |
missing_url | 400 | Scraper: URL missing. |
internal_error | 500 | Scraper: unexpected failure. |
scraper_not_configured, not_a_product_page, external_error, network_error, fetch_failed, fallback_scrape_failed | — | Scrape result codes returned inside the scrape payload (success: false), not as HTTP errors. |
Consumer app — /me/*, /amazon/*, /feedback, /roles
These routes serve the Share a Wish app; listed for completeness.
| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | User token missing or invalid. |
forbidden | 403 | Friend request does not belong to the user. |
not_found | 404 | Notification, shared wishlist, Amazon list or item unknown. |
not_found_or_not_pending | 404 | Friend request unknown or already handled. |
missing_params / missing_token / missing_name / missing_wishlist / missing_friend_id / missing_recipient | 400 | Required parameter missing. |
invalid_item_id | 400 | Item id not numeric. |
invalid_code | 404 | Invite code unknown. |
cannot_invite_self / self_request / cannot_unfriend_self | 400 | Friend actions targeting yourself. |
update_failed / delete_failed / insert_failed / db_error / server_error / internal_error | 500 | Database operation failed. |
missing_list_url / invalid_list_url / missing_amazon_list_id / missing_id / no_target_wishlist | 400 | Amazon import: request incomplete or URL not an Amazon list. |
wishlist_not_owned | 403 | Amazon import: target wishlist belongs to someone else. |
create_failed / sync_failed / internal | 500 | Amazon import failed. |
invalid_idea / idea_too_long / invalid_rating / invalid_category | 400 | Feedback payload invalid. |
submission_failed | 500 | Feedback could not be stored. |
Discontinued services
| Code | Status | Meaning |
|---|---|---|
ad_studio_disabled | 410 | The AI Ad Studio (/ad-studio/* function) has been discontinued and archived on 2026-08-30. Every request except GET /ad-studio/health and GET /ad-studio/credits answers 410 Gone with message: "The AI Ad Studio has been discontinued (archived 2026-08-30)." |