Documentation
With "Add to delelisten" your customers put your products on their shared delelisten.com shopping list. On the list, each product shows your shop name and a link back to the product page.
You need a free developer account and an app to get your keys. The examples below use placeholders.
1. How it works
- The customer presses "Add to delelisten" on your page or in your app.
- A delelisten.com window opens. If needed, the customer opens their My Lists there (PIN or social login), picks one of their lists and confirms. Nothing is added without that click.
- The products land on the list with your shop name and a link to the product. The window tells your page whether something was added and how many, and nothing more.
You have two keys per app:
- The public key (
pk_…) goes in your web pages and apps. It can only open the delelisten window, for links on your own domains. - The secret key (
sk_live_…) is only for your server and the server API. Never put it in a web page or an app; if it leaks, make a new one on the app page.
Most web shops only need the button. Use the server API when your server should put a whole cart together, or when you want to check afterwards whether the customer added it.
2. The button on your web shop
Load the script once per page, then mark any button or link with data-delelisten. An empty button gets the delelisten look and the text "Add to delelisten" in the visitor's language.
<script src="https://delelisten.com/app/public/button.js" data-key="pk_YOUR_PUBLIC_KEY" defer></script>
<button data-delelisten data-name="Organic milk 1 l" data-quantity="2"></button>
<!-- several products at once, e.g. a recipe or a cart -->
<button data-delelisten
data-items='[{"name":"Spaghetti 500 g","url":"https://myshop.com/p/11"},{"name":"Tomatoes","quantity":"6"}]'>
Put the recipe on my list
</button>| Attribute | What it does |
|---|---|
data-name | The product as it should read on the list (required, max 200 characters). |
data-quantity | For example 2 or 500 g (max 20 characters). |
data-url | Link to the product. Default: the page the button is on. It must be on your shop address or one of the domains in your app settings, otherwise the link is left out (the product is still added). |
data-note | A short note (max 120 characters). |
data-items | Several products as a JSON array of {"name", "quantity", "url", "note"}, at most 20. |
data-key | The public key, if it is not on the script tag (for example with several apps on one page). |
data-label | Your own text for an empty button. |
data-style | The look of an empty button: button (default: green with the delelisten bag), outline (white with a thin border), icon (only the delelisten bag logo, like a wish-list heart; the text shows as a tooltip and is read out by screen readers) or none (just the text, for your own CSS). A button with your own content is never restyled. |
data-size | With data-style="icon": the logo size in pixels (16 to 96, default 28). |
data-return | Where the delelisten window goes afterwards (https on your domains). Usually not needed on the web: the window just closes. |
The bag shows a + until the customer has added the product; then it turns into the check mark of the delelisten logo, and the icon's tooltip says "Added to delelisten". The text follows the language of your page (<html lang="da">), otherwise the visitor's browser.
<!-- Only the logo, next to your wish-list heart -->
<button data-delelisten data-style="icon" data-name="Smeg 50's Style kettle"></button>
<!-- White button with a border -->
<button data-delelisten data-style="outline" data-name="Smeg 50's Style kettle"></button>Buttons added to the page later (for example in a product grid loaded by script) are picked up with Delelisten.init(). A link with the class delelisten-add works as well as data-delelisten.
3. The recipe button for food blogs
Put it next to a recipe. Your readers get every ingredient on their shopping list, choose how many people they cook for (all amounts change with it), and untick what they already have. Salt, pepper and water start unticked. Ingredients already on their list add up: 3 eggs from your recipe and 2 eggs on the list become 5 eggs. On the list, each ingredient links back to your recipe.
<script src="https://delelisten.com/app/public/button.js" data-key="pk_YOUR_PUBLIC_KEY" defer></script>
<button data-delelisten data-type="recipe"></button>That is all for most food blogs: the button reads the recipe from the page itself. WordPress recipe plugins (WP Recipe Maker, Tasty Recipes, Mediavine Create and others), and most recipe sites, already add the recipe for search engines in the standard schema.org Recipe format, with recipeIngredient and recipeYield. The button uses the name, the servings and the ingredient lines from there.
No recipe data on your page? Give the ingredients on the button, one per line, or as a JSON array:
<button data-delelisten data-type="recipe"
data-title="Pancakes" data-servings="4"
data-ingredients='["2 dl flour", "3 eggs", "4 dl milk", "1 pinch of salt", "Butter for frying"]'></button>| Attribute | What it does |
|---|---|
data-type="recipe" | Makes it a recipe button: a bowl instead of the bag, and the text "Add ingredients to delelisten" in the visitor's language. |
data-ingredients | Optional. The ingredient lines as you write them in the recipe ("500 g flour", "1 onion, chopped"), at most 60. Amounts, units and remarks are understood in 13 languages. |
data-title, data-servings | Optional. The recipe's name, and how many people it is for. Without servings, the amounts can't be changed. |
data-recipe | Optional. With several recipes on one page: the name of the recipe this button belongs to. |
data-style, data-label, data-size and data-key work as on the shop button. From your own script: Delelisten.addRecipe() (the recipe on the page) or Delelisten.addRecipe({ title, servings, ingredients: [...] }). The server API takes a recipe too (see below).
<!-- Only the bowl icon, e.g. next to "Print" and "Pin" -->
<button data-delelisten data-type="recipe" data-style="icon"></button>
4. The cart button
For the cart page of a web shop: the whole cart goes on the customer's list in one go, up to 60 products, each with a tick box so they can leave out what they don't need. Products already on their list add up.
<button data-delelisten data-type="cart"
data-items='[{"name":"Organic milk 1 l","quantity":"2","url":"https://myshop.com/p/123"},
{"name":"Rye bread","url":"https://myshop.com/p/456"}]'></button>It shows a cart instead of the bag and the text "Add my cart to delelisten". If your cart changes on the page, call it from your own script instead: Delelisten.addCart(products). For very large carts (long product names and links), the server API is safer: a link in the browser can only be so long, so the button leaves out product links first and then the last products.
5. From your own JavaScript
// After button.js has loaded:
Delelisten.add({ name: "Organic milk 1 l", quantity: "2", url: "https://myshop.com/p/123" });
Delelisten.add([{ name: "Spaghetti 500 g" }, { name: "Tomatoes", quantity: "6" }]);
// When the customer is done (the event bubbles up from the button that was pressed):
document.addEventListener("delelisten:done", (event) => {
if (event.detail.status === "added") {
showMessage(event.detail.count + " product(s) are now on your delelisten list");
}
// event.detail.status is "added" or "cancelled"
});Call Delelisten.add directly from a click handler, so the browser allows the window. If pop-ups are blocked, delelisten opens in a new tab instead.
6. Add links (apps and anything else)
The button only builds a link. You can build it yourself, for example in an app, an e-mail or a QR code on a shelf label:
https://delelisten.com/app/public/add.php?k=pk_YOUR_PUBLIC_KEY&n=Organic%20milk%201%20l&q=2&u=https%3A%2F%2Fmyshop.com%2Fp%2F123| Parameter | Meaning |
|---|---|
k | Your public key (required). |
n, q, u, note | Name, quantity, product link and note of one product. |
items | Instead of n/q/u: several products as a JSON array, encoded as base64url (URL-safe base64 without =). Add type=cart for a whole cart. |
recipe | Instead of products: a recipe as base64url JSON {"t": title, "s": servings, "u": recipe link, "l": ["500 g flour", …]}. |
return | Where to go afterwards: https on your domains, or an address that starts with one of your app's return addresses (for example myshop://delelisten). delelisten adds delelisten=added&count=2 or delelisten=cancelled&count=0. |
Encode every value (spaces as %20, not +).
7. The server API
Address: https://delelisten.com/app/public/shop-api.php. Send the secret key in the header X-Api-Key (or Authorization: Bearer sk_live_…). Answers are JSON in UTF-8.
Check your key
GET https://delelisten.com/app/public/shop-api.php?r=ping
-> 200 {"ok": true, "app": {"name": "My Shop", "domains": ["myshop.com"]}}Create an add request
Your server sends the products; you get a link that is valid for 24 hours. Send the customer to that link (redirect, new window or in-app browser). The customer picks a list and confirms; the link then cannot be used again.
POST https://delelisten.com/app/public/shop-api.php?r=add-requests
Content-Type: application/json
X-Api-Key: sk_live_…
{
"items": [
{"name": "Organic milk 1 l", "quantity": "2", "url": "https://myshop.com/p/123", "note": "the blue one"},
{"name": "Rye bread"}
],
"return_url": "https://myshop.com/cart",
"reference": "cart-981"
}
-> 201
{
"id": "ar_…",
"status": "pending",
"url": "https://delelisten.com/app/public/add.php?r=ar_…",
"item_count": 2,
"added_count": 0,
"reference": "cart-981",
"created_at": "2026-10-11T09:30:00+00:00",
"expires_at": "2026-10-12T09:30:00+00:00",
"completed_at": null
}items is required (1 to 20 products, each with a name; a whole cart: up to 60). return_url and reference (your own id, max 100 characters) are optional. If a product link is not on your domains, it is left out and the answer has a warnings list.
For a recipe, send recipe instead of items. The customer then ticks the ingredients and chooses the servings on delelisten; item_count is the number of ingredient lines.
{
"recipe": {
"title": "Pancakes",
"servings": 4,
"url": "https://myfoodblog.com/pancakes",
"ingredients": ["2 dl flour", "3 eggs", "4 dl milk", "1 pinch of salt"]
},
"return_url": "https://myfoodblog.com/pancakes"
}Check an add request
GET https://delelisten.com/app/public/shop-api.php?r=add-requests&id=ar_…
-> 200 {"id": "ar_…", "status": "added", "added_count": 2, ...}status is pending, added, cancelled or expired. Requests are kept for 30 days.
Examples
<?php
// Cart page: "Put my cart on my delelisten list"
$secret = getenv('DELELISTEN_SECRET'); // sk_live_… (never in a web page or app)
$body = [
'items' => [
['name' => 'Organic milk 1 l', 'quantity' => '2', 'url' => 'https://myshop.com/p/123'],
['name' => 'Rye bread', 'url' => 'https://myshop.com/p/456'],
],
'return_url' => 'https://myshop.com/cart', // where the customer comes back to
'reference' => 'cart-981', // your own id, optional
];
$ch = curl_init('https://delelisten.com/app/public/shop-api.php?r=add-requests');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Api-Key: ' . $secret],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$answer = json_decode((string) curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status === 201) {
header('Location: ' . $answer['url']); // the customer picks a list, then returns
exit;
}
error_log('delelisten: ' . ($answer['error']['message'] ?? 'no answer'));
// Back on https://myshop.com/cart?delelisten=added&count=2
if (($_GET['delelisten'] ?? '') === 'added') {
echo (int) $_GET['count'] . ' products are now on your delelisten list.';
}import json
import os
import urllib.error
import urllib.request
API = "https://delelisten.com/app/public/shop-api.php"
SECRET = os.environ["DELELISTEN_SECRET"] # sk_live_… (never in a web page or app)
def call(method, route, body=None):
req = urllib.request.Request(
f"{API}?r={route}",
data=json.dumps(body).encode() if body is not None else None,
method=method,
headers={"Content-Type": "application/json", "X-Api-Key": SECRET},
)
try:
with urllib.request.urlopen(req, timeout=10) as res:
return json.load(res)
except urllib.error.HTTPError as err:
raise RuntimeError(json.load(err)["error"]["message"]) from err
request = call("POST", "add-requests", {
"items": [
{"name": "Organic milk 1 l", "quantity": "2", "url": "https://myshop.com/p/123"},
{"name": "Rye bread", "url": "https://myshop.com/p/456"},
],
"return_url": "https://myshop.com/cart",
"reference": "cart-981",
})
print(request["url"]) # send the customer here (redirect, new window or in-app browser)
# Later: did the customer add it?
print(call("GET", "add-requests&id=" + request["id"])["status"]) # pending | added | cancelled | expired// Node.js 18+ (built-in fetch)
const API = "https://delelisten.com/app/public/shop-api.php";
const SECRET = process.env.DELELISTEN_SECRET; // sk_live_… (never in a web page or app)
async function createAddRequest(items, returnUrl, reference) {
const res = await fetch(`${API}?r=add-requests`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Api-Key": SECRET },
body: JSON.stringify({ items, return_url: returnUrl, reference }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error.message);
return data; // { id, url, status, expires_at, ... }
}
// Express: a "Put my cart on delelisten" button posts here
app.post("/cart/delelisten", async (req, res) => {
const items = req.session.cart.map((p) => ({ name: p.title, quantity: String(p.qty), url: p.url }));
const request = await createAddRequest(items, "https://myshop.com/cart", req.session.cartId);
res.redirect(303, request.url);
});// Java 11+ (java.net.http)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class Delelisten {
static final String API = "https://delelisten.com/app/public/shop-api.php";
static final String SECRET = System.getenv("DELELISTEN_SECRET"); // sk_live_…
static final HttpClient HTTP = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
/** Returns the JSON answer. Its "url" is where the customer picks a list. */
public static String createAddRequest(String json) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(API + "?r=add-requests"))
.header("Content-Type", "application/json")
.header("X-Api-Key", SECRET)
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 201) {
throw new IllegalStateException("delelisten: " + res.body());
}
return res.body(); // read "url" with your JSON library (Jackson, Gson, ...)
}
public static void main(String[] args) throws Exception {
String json = """
{"items": [{"name": "Organic milk 1 l", "quantity": "2", "url": "https://myshop.com/p/123"}],
"return_url": "https://myshop.com/cart", "reference": "cart-981"}""";
System.out.println(createAddRequest(json));
}
}package delelisten
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
const api = "https://delelisten.com/app/public/shop-api.php"
type Item struct {
Name string `json:"name"`
Quantity string `json:"quantity,omitempty"`
URL string `json:"url,omitempty"`
Note string `json:"note,omitempty"`
}
type AddRequest struct {
ID string `json:"id"`
URL string `json:"url"`
Status string `json:"status"`
AddedCount int `json:"added_count"`
}
var client = &http.Client{Timeout: 10 * time.Second}
func CreateAddRequest(items []Item, returnURL, reference string) (*AddRequest, error) {
body, _ := json.Marshal(map[string]any{"items": items, "return_url": returnURL, "reference": reference})
req, _ := http.NewRequest(http.MethodPost, api+"?r=add-requests", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Api-Key", os.Getenv("DELELISTEN_SECRET")) // sk_live_…
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
var e struct {
Error struct{ Message string } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&e)
return nil, fmt.Errorf("delelisten: %s", e.Error.Message)
}
var r AddRequest
err = json.NewDecoder(res.Body).Decode(&r)
return &r, err
}
// In your handler:
// r, err := delelisten.CreateAddRequest(items, "https://myshop.com/cart", cartID)
// http.Redirect(w, req, r.URL, http.StatusSeeOther)// .NET 6+
using System.Net.Http.Json;
using System.Text.Json;
var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
http.DefaultRequestHeaders.Add("X-Api-Key", Environment.GetEnvironmentVariable("DELELISTEN_SECRET")); // sk_live_…
var res = await http.PostAsJsonAsync("https://delelisten.com/app/public/shop-api.php?r=add-requests", new
{
items = new[] { new { name = "Organic milk 1 l", quantity = "2", url = "https://myshop.com/p/123" } },
return_url = "https://myshop.com/cart",
reference = "cart-981",
});
var data = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!res.IsSuccessStatusCode)
throw new Exception(data.GetProperty("error").GetProperty("message").GetString());
var addUrl = data.GetProperty("url").GetString(); // send the customer hererequire "net/http"
require "json"
uri = URI("https://delelisten.com/app/public/shop-api.php?r=add-requests")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json",
"X-Api-Key" => ENV.fetch("DELELISTEN_SECRET")) # sk_live_…
req.body = {
items: [{ name: "Organic milk 1 l", quantity: "2", url: "https://myshop.com/p/123" }],
return_url: "https://myshop.com/cart",
reference: "cart-981"
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
data = JSON.parse(res.body)
raise data["error"]["message"] unless res.code == "201"
redirect_to data["url"], allow_other_host: true # Rails: send the customer to delelistencurl -X POST "https://delelisten.com/app/public/shop-api.php?r=add-requests" \
-H "X-Api-Key: $DELELISTEN_SECRET" \
-H "Content-Type: application/json" \
-d '{"items":[{"name":"Organic milk 1 l","quantity":"2","url":"https://myshop.com/p/123"}],
"return_url":"https://myshop.com/cart","reference":"cart-981"}'
curl "https://delelisten.com/app/public/shop-api.php?r=add-requests&id=ar_THE_ID" -H "X-Api-Key: $DELELISTEN_SECRET"8. Mobile apps
Apps use the add link with the public key (or a link your server got from the server API). Open it in the system's secure browser sheet, not in a WebView: there the customer is already signed in to delelisten. Register your return address (for example myshop://delelisten) under "Return addresses" in the app settings, so delelisten can send the customer back to your app.
import AuthenticationServices
import UIKit
/// Opens delelisten in a secure browser sheet. The customer stays signed in to delelisten (shared with Safari).
final class DelelistenButton: NSObject, ASWebAuthenticationPresentationContextProviding {
static let publicKey = "pk_YOUR_PUBLIC_KEY"
static let scheme = "myshop" // register "myshop://delelisten" under Return addresses
private var session: ASWebAuthenticationSession?
private weak var window: UIWindow?
func add(name: String, quantity: String = "", productURL: String = "", from window: UIWindow?,
done: @escaping (_ added: Int) -> Void) {
var c = URLComponents(string: "https://delelisten.com/app/public/add.php")!
c.queryItems = [
URLQueryItem(name: "k", value: Self.publicKey),
URLQueryItem(name: "n", value: name),
URLQueryItem(name: "q", value: quantity),
URLQueryItem(name: "u", value: productURL),
URLQueryItem(name: "return", value: "\(Self.scheme)://delelisten"),
]
c.percentEncodedQuery = c.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
self.window = window
session = ASWebAuthenticationSession(url: c.url!, callbackURLScheme: Self.scheme) { url, _ in
// url = myshop://delelisten?delelisten=added&count=1 (nil when the customer closed the sheet)
let items = url.flatMap { URLComponents(url: $0, resolvingAgainstBaseURL: false)?.queryItems } ?? []
let added = items.first { $0.name == "delelisten" }?.value == "added"
done(added ? Int(items.first { $0.name == "count" }?.value ?? "0") ?? 0 : 0)
}
session?.presentationContextProvider = self
session?.prefersEphemeralWebBrowserSession = false
session?.start()
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
window ?? ASPresentationAnchor()
}
}// build.gradle: implementation("androidx.browser:browser:1.8.0")
import android.content.Context
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
/** Opens delelisten in a Custom Tab. The customer stays signed in to delelisten (shared with Chrome). */
fun addToDelelisten(context: Context, name: String, quantity: String = "", productUrl: String = "") {
val uri = Uri.parse("https://delelisten.com/app/public/add.php").buildUpon()
.appendQueryParameter("k", "pk_YOUR_PUBLIC_KEY")
.appendQueryParameter("n", name)
.appendQueryParameter("q", quantity)
.appendQueryParameter("u", productUrl)
.appendQueryParameter("return", "myshop://delelisten") // registered under Return addresses
.build()
CustomTabsIntent.Builder().build().launchUrl(context, uri)
}
/* AndroidManifest.xml: catch the way back ("Back to My Shop" in delelisten)
<activity android:name=".DelelistenReturnActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myshop" android:host="delelisten" />
</intent-filter>
</activity>
In DelelistenReturnActivity:
val status = intent.data?.getQueryParameter("delelisten") // "added" or "cancelled"
val count = intent.data?.getQueryParameter("count")?.toIntOrNull() ?: 0
*/// The same add link works from any app framework. Open it in the system's in-app browser
// (not in a WebView, where the customer is not signed in to delelisten), and listen for
// your return address, e.g. myshop://delelisten?delelisten=added&count=1
// Flutter (package flutter_web_auth_2):
final result = await FlutterWebAuth2.authenticate(
url: Uri.parse('https://delelisten.com/app/public/add.php').replace(queryParameters: {
'k': 'pk_YOUR_PUBLIC_KEY', 'n': 'Organic milk 1 l', 'q': '2',
'u': 'https://myshop.com/p/123', 'return': 'myshop://delelisten',
}).toString(),
callbackUrlScheme: 'myshop',
);
// React Native (Expo, expo-web-browser):
const params = new URLSearchParams({ k: 'pk_YOUR_PUBLIC_KEY', n: 'Organic milk 1 l', q: '2',
u: 'https://myshop.com/p/123', return: 'myshop://delelisten' });
const result = await WebBrowser.openAuthSessionAsync('https://delelisten.com/app/public/add.php?' + params, 'myshop://delelisten');
// result.type === 'success' -> new URL(result.url).searchParams.get('delelisten')9. Errors and limits
Errors come as {"error": {"code": "…", "message": "…"}} with a matching HTTP status.
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_key | No key in the request. |
| 401 | wrong_key | You sent the public key; the server API needs the secret key. |
| 401 | invalid_key | Unknown secret key (maybe replaced by a new one). |
| 403 | not_active | The app is paused, or the account is waiting for approval or paused. |
| 400 | bad_json | The body is not JSON. |
| 422 | no_items, too_many_items | No product with a name, or more than 20 (60 for a whole cart). |
| 422 | no_ingredients | A recipe without any ingredient line that could be read. |
| 422 | bad_return_url | The return address is not https on your domains and not one of your return addresses. |
| 400 / 404 | bad_id, not_found | Unknown add request. |
| 429 | rate_limited | Too many requests. Wait a minute. |
Limits: 20 products per add, 600 API requests per minute per app, 10 apps per account. A list holds at most 500 products; when it is full, fewer are added and the count says how many.
10. Rules and privacy
- Use the button for your own products, with honest names, and never try to add anything without the customer's own click.
- delelisten.com never tells you who the customer is, which lists they have, what else is on them or their PIN. You only learn whether something was added, and how many products.
- On the list, delelisten stores the product, your shop name and the product link. The customer can change or delete them like any other product.
- delelisten.com can pause an app or account that breaks these rules.