REST API
Plan: Premium.
A JSON API for your servers: an ERP, a CRM, a marketing tool or a headless storefront’s backend. Read a customer’s lists, save and remove items for them, and see your most-saved products.
- Base URL:
https://covewishlist.com/api/v1 - OpenAPI 3.1 document: https://covewishlist.com/developers/openapi.json (import it into Postman, Insomnia or a code generator).
- JSON in and out. Every response, success or error, carries
Cache-Control: no-store. - Customers are identified by their numeric Shopify customer ID only. The API never accepts or returns an email, name or any other customer detail, and a request body with a field it doesn’t know (an
email, say) is refused with400 invalid_request. - Every operation is idempotent: saving an item twice, or removing something that isn’t saved, changes nothing. Retrying is always safe.
- IDs are strings in responses (JSON numbers can’t hold every 64-bit ID).
Authentication
Section titled “Authentication”Create a key
Section titled “Create a key”- In Cove, go to Settings › Account. In the Developer API section, click Manage keys. This opens the Developers page.
- Under New key, give the key a Name (after whatever will use it; only you see it) and choose its Access:
- Read: lists, items and most-saved products (the
GETendpoints). - Read and write: also save and remove items.
- Read: lists, items and most-saved products (the
- Click Create key. The key is shown once, in a Your new API key banner with a Copy key button. Copy it now and keep it on your server.
Keys look like cove_live_ followed by 32 characters, for example cove_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. Cove stores only a one-way hash of the key and a short prefix to show in the list, so the key can’t be shown again or recovered: if you lose it, revoke it and create another.
The key list shows each key’s name, prefix, access, when it was created and when it was last used. A shop can have up to 10 active keys.
Revoke a key
Section titled “Revoke a key”Click Revoke next to the key and confirm. It stops working at once, and anything using it loses access. This can’t be undone. Every key is also revoked automatically when the app is uninstalled.
Send the key
Section titled “Send the key”Send the key as a bearer token in the Authorization header:
curl https://covewishlist.com/api/v1/me \ -H "Authorization: Bearer $COVE_API_KEY"A missing header answers 401 missing_api_key; a malformed, unknown or revoked key (or one for a shop that uninstalled the app) answers 401 invalid_api_key, with a WWW-Authenticate: Bearer header. The API doesn’t tell those cases apart.
Plan changes
Section titled “Plan changes”Keys work while your plan includes the developer API (Premium). After a downgrade, every call answers 403 plan_required and Settings › Account shows your keys as paused; the keys themselves are kept, and they work again as soon as you’re back on Premium. You can revoke keys on any plan, but you can only create them on Premium. See changing plans.
Rate limits
Section titled “Rate limits”- 120 requests a minute per key, and 600 a minute per shop across all its keys.
- Every authenticated response carries the key’s window:
| Header | What it holds |
|---|---|
X-RateLimit-Limit |
Requests this key may make per minute. |
X-RateLimit-Remaining |
Requests left in the current minute. |
X-RateLimit-Reset |
When the window resets (Unix seconds). |
- Over a limit, the API answers
429 rate_limitedwith aRetry-Afterheader in seconds. Back off and retry. - Repeated failed authentication from one IP address is limited too: after 30 failures in a minute it answers
429 rate_limitedwithRetry-After: 60.
Wishlist actions quota
Section titled “Wishlist actions quota”Each new save through POST /customers/{customerId}/items counts as one wishlist action toward the shop’s monthly quota, exactly like a save on the storefront. Saving something that’s already saved uses nothing, and if a save fails the action is handed back. At the limit, adds answer 429 quota_reached until the next month or an upgrade; reads and removals keep working. See wishlist actions.
Errors
Section titled “Errors”Errors are JSON with the HTTP status below:
{ "error": "list_not_found" }Match on the error code, not on any message. plan_required also carries feature (developerApi) and plan (the lowest plan that includes it, PREMIUM).
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request |
The body or query failed validation (unknown fields included). |
| 400 | invalid_json |
The body isn’t JSON. |
| 400 | invalid_customer_id |
The customer ID isn’t a numeric Shopify customer ID. |
| 400 | invalid_product_id |
The product ID isn’t a numeric Shopify product ID. |
| 400 | invalid_limit |
limit is not an integer from 1 to 250. |
| 401 | missing_api_key |
No Authorization header. |
| 401 | invalid_api_key |
The key is malformed, unknown or revoked, or the app is uninstalled. |
| 403 | plan_required |
The shop’s plan doesn’t include the developer API. Also carries feature and plan. |
| 403 | insufficient_scope |
A write with a read-only key. |
| 404 | list_not_found |
The listId isn’t one of this customer’s lists. |
| 404 | not_found |
No such endpoint. |
| 405 | method_not_allowed |
Wrong HTTP method; Allow names the right one. |
| 409 | list_full |
The list already holds 500 items. |
| 413 | body_too_large |
The body is over 16 KB. |
| 429 | rate_limited |
Too many requests; wait Retry-After seconds. |
| 429 | quota_reached |
The shop has used its monthly wishlist actions; adds are refused until the next month or an upgrade. |
| 500 | internal_error |
Our fault. Safe to retry: every operation is idempotent. |
| 503 | paused |
The app’s storefront features are paused on this shop. |
Every endpoint can answer 401, 403, 429 and 503 as above, besides the errors listed under it.
| Where | Accepted |
|---|---|
customerId and productId in a path |
Numeric Shopify ID only: 1 to 19 digits, no leading zero, at most 9223372036854775807. Not a GID, never an email. |
variantId in a query string |
Numeric ID or GID (gid://shopify/ProductVariant/…). |
productId and variantId in a request body |
A numeric string, a JSON number (safe integers only, up to 9007199254740991) or a GID (gid://shopify/Product/…, gid://shopify/ProductVariant/…). |
listId |
Cove’s list ID, as returned by the API: 20 to 32 lowercase letters and digits. |
Endpoints
Section titled “Endpoints”GET /me
Section titled “GET /me”The shop and access of the key. Use it to check credentials. Needs a Read or Read and write key.
curl https://covewishlist.com/api/v1/me \ -H "Authorization: Bearer $COVE_API_KEY"200:
{ "shop": "your-store.myshopify.com", "scope": "READ_WRITE" }scope is READ or READ_WRITE.
GET /customers/{customerId}/wishlist
Section titled “GET /customers/{customerId}/wishlist”A customer’s lists and saved items. Needs a Read or Read and write key.
| Parameter | In | Required | What it is |
|---|---|---|---|
customerId |
path | yes | Numeric Shopify customer ID. |
Every list with its live items, newest first. A customer who never saved anything gets "lists": [], never a 404, and reading creates nothing.
curl https://covewishlist.com/api/v1/customers/7012345678901/wishlist \ -H "Authorization: Bearer $COVE_API_KEY"200:
{ "customerId": "7012345678901", "count": 2, "lists": [ { "id": "clx0examplelistid0000", "name": "My wishlist", "isDefault": true, "shared": false, "createdAt": "2026-10-01T09:30:00.000Z", "count": 2, "items": [ { "productId": "8123456789", "variantId": "45123456789", "quantity": 1, "addedAt": "2026-10-20T18:02:11.000Z" }, { "productId": "8123456790", "variantId": null, "quantity": 1, "addedAt": "2026-10-02T10:00:00.000Z" } ] } ]}| Field | What it holds |
|---|---|
customerId |
The customer ID you asked for. |
count |
Distinct saved products and variants across all lists (an item in two lists counts once). |
lists[].id |
Cove’s list ID. |
lists[].name |
The list’s name. It’s shopper text: show it as text, never as HTML. |
lists[].isDefault |
Where one-tap storefront saves go. |
lists[].shared |
Whether the list has a live share link. The link itself is never returned. |
lists[].createdAt |
When the list was created. |
lists[].count |
Items in this list. |
lists[].items[].productId |
Shopify product ID. |
lists[].items[].variantId |
Shopify variant ID, or null when the product was saved without a variant. |
lists[].items[].quantity |
Quantity. |
lists[].items[].addedAt |
When it was saved. |
Shoppers’ private notes are never returned.
Errors: 400 invalid_customer_id.
POST /customers/{customerId}/items
Section titled “POST /customers/{customerId}/items”Save a product, or one variant of it, for a customer. Needs a Read and write key.
| Parameter | In | Required | What it is |
|---|---|---|---|
customerId |
path | yes | Numeric Shopify customer ID. |
productId |
body | yes | The product to save. |
variantId |
body | no | The variant to save. Omit it or send null for a product-level save. |
listId |
body | no | The list to save to, which must be one of this customer’s. |
Without listId, the item goes to the customer’s default list, which is created on first use. With it, the item goes to that list; unlike a save on the storefront’s list picker, this doesn’t make that list the default. The body accepts only these three fields.
curl -X POST https://covewishlist.com/api/v1/customers/7012345678901/items \ -H "Authorization: Bearer $COVE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "productId": "8123456789", "variantId": "45123456789" }'201 when it was saved, 200 when it was already in that list and nothing changed. Both carry added and the customer’s wishlist after the call, in the same shape as GET /customers/{customerId}/wishlist:
{ "added": true, "wishlist": { "customerId": "7012345678901", "count": 1, "lists": [ { "id": "clx0examplelistid0000", "name": "My wishlist", "isDefault": true, "shared": false, "createdAt": "2026-10-20T18:02:11.000Z", "count": 1, "items": [ { "productId": "8123456789", "variantId": "45123456789", "quantity": 1, "addedAt": "2026-10-20T18:02:11.000Z" } ] } ] }}A new save counts as one wishlist action (see quota). A list holds up to 500 items.
Errors: 400 invalid_request, 400 invalid_json, 400 invalid_customer_id, 403 insufficient_scope, 404 list_not_found, 409 list_full, 413 body_too_large, 429 rate_limited, 429 quota_reached.
DELETE /customers/{customerId}/items/{productId}
Section titled “DELETE /customers/{customerId}/items/{productId}”Remove a saved item for a customer. Needs a Read and write key.
| Parameter | In | Required | What it is |
|---|---|---|---|
customerId |
path | yes | Numeric Shopify customer ID. |
productId |
path | yes | Numeric Shopify product ID. |
variantId |
query | no | Remove the save of this variant (numeric ID or GID). Without it, the product-level save is removed. |
listId |
query | no | Remove it from this list only. Without it, the item leaves every list (like the storefront heart). |
The removal is exact: a variant’s save and a product-level save are separate, so remove each one you mean to.
curl -X DELETE "https://covewishlist.com/api/v1/customers/7012345678901/items/8123456789?variantId=45123456789" \ -H "Authorization: Bearer $COVE_API_KEY"200, with removed, the number of saved rows removed (an item can sit in several lists, so it can be more than 1, and it is 0 when nothing was saved), and the wishlist after the call:
{ "removed": 1, "wishlist": { "customerId": "7012345678901", "count": 0, "lists": [ { "id": "clx0examplelistid0000", "name": "My wishlist", "isDefault": true, "shared": false, "createdAt": "2026-10-20T18:02:11.000Z", "count": 0, "items": [] } ] }}Removals never use a wishlist action.
Errors: 400 invalid_request (an unknown query parameter, a bad variantId or listId), 400 invalid_customer_id, 400 invalid_product_id, 403 insufficient_scope, 404 list_not_found.
GET /products/most-saved
Section titled “GET /products/most-saved”The shop’s products by how many shoppers have them saved now. Needs a Read or Read and write key.
| Parameter | In | Required | What it is |
|---|---|---|---|
limit |
query | no | How many products, 1 to 250. Default 50. |
curl "https://covewishlist.com/api/v1/products/most-saved?limit=10" \ -H "Authorization: Bearer $COVE_API_KEY"200, most-saved first:
{ "products": [{ "productId": "8123456789", "shoppers": 42, "saves": 47 }] }| Field | What it holds |
|---|---|
productId |
Shopify product ID. |
shoppers |
Shoppers with it saved now. A shopper counts once, however many lists hold it. |
saves |
Live saved rows (variants and lists counted separately). |
Counts only: never who saved.
Errors: 400 invalid_limit.
Anything else
Section titled “Anything else”Any other path under /api/v1 answers 404 not_found in JSON. The right path with the wrong method answers 405 method_not_allowed with an Allow header naming the method it takes.
Example: Node.js
Section titled “Example: Node.js”const BASE = 'https://covewishlist.com/api/v1';
async function cove(path, init = {}) { const res = await fetch(BASE + path, { ...init, headers: { Authorization: `Bearer ${process.env.COVE_API_KEY}`, 'Content-Type': 'application/json', ...init.headers, }, }); const body = await res.json(); if (!res.ok) { const error = new Error(`Cove API ${res.status}: ${body.error}`); error.code = body.error; error.retryAfter = Number(res.headers.get('Retry-After')) || null; throw error; } return body;}
// Save a product for a customer, then read their wishlist.await cove('/customers/7012345678901/items', { method: 'POST', body: JSON.stringify({ productId: '8123456789' }),});const { count, lists } = await cove('/customers/7012345678901/wishlist');