94 operations. Every schema and example on this page is generated from the platform contract.
Every drop for the authenticated store, newest first, each with its items, windows and live counters. Also returns whether the store can run a drop at all, and if not, why.
Include drops whose collection date has passed.
The short code that identifies this drop.
The shop's permanent page, at its own address: https://{handle}.membber.com/
The link to this specific drop.
draftscheduledcancelledComputed at read time. Never sent by a client.
draftscheduledliveclosedcollectedcancelledNull only while a draft has no closing time yet. A scheduled drop always has one.
Copied from the menu item at read time, never stored twice. On a line that sells a size or colour: the item’s name with them, "Hoodie, M, Black".
Her item’s own name, on a line that sells one of its sizes or colours (`name` then reads "Hoodie, M, Black").
The size this line sells, as her item names it. Absent: no size.
The colour this line sells, as her item names it. Absent: no colour.
Her own word for this item’s sizes ("Flavour"), when she chose one. Absent: the word for size.
Her own word for this item’s colours ("Grind"), when she chose one. Absent: the word for colour.
The photo she chose for this colour, one of the item’s own photos. Absent: none chosen.
What one sells at in this drop, before Membber's fee: the drop's own price when it has one, else the menu price. What checkout charges.
Her usual (menu) price for one.
This drop's own price for one, or null when it sells at the usual price.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
How many of this item go in one box of a one-price drop. Null means one.
Always null for an unlimited item.
False for an item that is not food (clothes, flowers): no allergen answer is asked of it. True or absent: food.
Send it back on the windows PUT to edit this window in place.
null = no cap on orders in this window.
People still to collect from this window. Moving it needs confirm_booked_changes; removing it is refused.
First names of the people still to collect, oldest order first.
False when anyone holds a place in this window, collected or not: removing it is refused.
True when the only orders holding this window are disputed ones nobody collected. Absent otherwise.
Buyers still to collect whom a moved collection time could not reach. Absent when there are none.
How many buyers could not be reached about a moved time.
Their first names, oldest order first, three at most (the most the sentence names). Shorter than not_told when one gave no name or there are more than three; absent when none gave one.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Orders refunded in full, disputed by the buyer, released unpaid or cancelled with the drop, newest first. Listed only: never in buyers and never in any count or total.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Sum of the counted items only; an unlimited item adds nothing.
TAKEN: money that has arrived and stayed, from every paid status (paid, ready, collected, part-refunded), net of recorded refunds. Card orders without the buyer fee; cash and transfer in full.
How many boxes were put up.
How many boxes are left. Stock, never writable.
True when this drop sells one box at one price.
no_itemsno_timesno_closesno_quantityno_priceno_allergenstime_before_openstime_before_closestime_off_dayWhy a drop cannot open. Say this to the merchant, do not refuse blankly.
no_addoncollect_offno_paymentspayments_destinationunavailableWhether the shop is live. A drop can only be scheduled on a live shop. Null when unreadable.
Pay now is on but cards are not set up yet: this real shop is still on its old practice Stripe account, which takes no card. "Set up cards" makes its live account.
pay_nowbank_transfercashVisits to this shop's public drops pages. Null only when the count could not be read.
True while downloads are switched on: a seller screen then shows its Downloads row. Absent otherwise.
curl -G "https://www.membber.com/api/v1/drops" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.listBatchDrops(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"drops": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"code": "EXAMPLE10",
"shop_url": "https://example.com/image.jpg",
"image_url": "https://example.com/image.jpg",
"share_url": "https://example.com/image.jpg",
"name": "Example name",
"description": "Added at the front desk",
"status": "draft",
"state": "draft",
"opens_at": "<opens_at>",
"closes_at": "<closes_at>",
"collection_date": "<collection_date>",
"timezone": "Europe/London",
"checkout_ttl_seconds": 1,
"menu_visible_before_open": true,
"show_stock_count": true,
"max_per_customer": 1,
"items": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"item_name": "<item_name>",
"size_label": "<size_label>",
"colour_label": "<colour_label>",
"size_name": "<size_name>",
"colour_name": "<colour_name>",
"colour_photo_url": "https://example.com/image.jpg",
"price_pence": 1500,
"usual_price_pence": 1500,
"drop_price_pence": 1500,
"buyer_price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"per_bundle": -9007199254740991,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true,
"is_food": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1,
"booked_count": 1,
"booked_names": [
"<booked_name>"
],
"removable": true,
"in_dispute": true
}
],
"moves_not_reached": {
"not_told": -9007199254740991,
"not_told_names": [
"<not_told_name>"
]
},
"buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"cancelled_buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"still_to_come_pence": 1500,
"unpaid_orders_count": 1,
"bundle_price_pence": 1500,
"bundle_buyer_price_pence": 1500,
"bundle_total": 1,
"bundle_remaining": 1,
"is_bundle": true,
"blockers": [
"<blocker>"
],
"blocker_codes": [
{
"code": "no_items",
"names": [
"Example name"
]
}
],
"making_started_at": "<making_started_at>",
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
],
"live_drop_id": "68f0d418-0000-4000-8000-d0c500000068",
"drops_live": true,
"blocked_reason": "no_addon",
"shop_live": true,
"card_practice_only": true,
"payment_methods": [
"pay_now"
],
"reachable_customers": -9007199254740991,
"page_views": {
"shop_today": -9007199254740991,
"shop_last_7_days": -9007199254740991,
"drops_today": -9007199254740991,
"drops_last_7_days": -9007199254740991
},
"downloads_enabled": true
}/api/v1/dropsCreates a DRAFT drop for the authenticated store. A draft is invisible to customers and has no link until it is scheduled. Send a `client_token` to make the create idempotent.
Send the same token on a retry to get the SAME drop back instead of a duplicate.
ISO instant. The drop opens itself at this minute.
ISO instant. Null is allowed on a DRAFT (nothing is on sale yet, so nothing closes), but a drop cannot be scheduled without a closing time. It may be sent in the same PATCH as status=scheduled.
YYYY-MM-DD in the store timezone.
What the whole box costs, in pence. Null means this is not a box drop.
How many boxes exist. Null means this is not a box drop.
Basket hold, in seconds. Defaults to 300 (5 minutes).
null = no limit.
The short code that identifies this drop.
The shop's permanent page, at its own address: https://{handle}.membber.com/
The link to this specific drop.
draftscheduledcancelledComputed at read time. Never sent by a client.
draftscheduledliveclosedcollectedcancelledNull only while a draft has no closing time yet. A scheduled drop always has one.
Copied from the menu item at read time, never stored twice. On a line that sells a size or colour: the item’s name with them, "Hoodie, M, Black".
Her item’s own name, on a line that sells one of its sizes or colours (`name` then reads "Hoodie, M, Black").
The size this line sells, as her item names it. Absent: no size.
The colour this line sells, as her item names it. Absent: no colour.
Her own word for this item’s sizes ("Flavour"), when she chose one. Absent: the word for size.
Her own word for this item’s colours ("Grind"), when she chose one. Absent: the word for colour.
The photo she chose for this colour, one of the item’s own photos. Absent: none chosen.
What one sells at in this drop, before Membber's fee: the drop's own price when it has one, else the menu price. What checkout charges.
Her usual (menu) price for one.
This drop's own price for one, or null when it sells at the usual price.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
How many of this item go in one box of a one-price drop. Null means one.
Always null for an unlimited item.
False for an item that is not food (clothes, flowers): no allergen answer is asked of it. True or absent: food.
Send it back on the windows PUT to edit this window in place.
null = no cap on orders in this window.
People still to collect from this window. Moving it needs confirm_booked_changes; removing it is refused.
First names of the people still to collect, oldest order first.
False when anyone holds a place in this window, collected or not: removing it is refused.
True when the only orders holding this window are disputed ones nobody collected. Absent otherwise.
Buyers still to collect whom a moved collection time could not reach. Absent when there are none.
How many buyers could not be reached about a moved time.
Their first names, oldest order first, three at most (the most the sentence names). Shorter than not_told when one gave no name or there are more than three; absent when none gave one.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Orders refunded in full, disputed by the buyer, released unpaid or cancelled with the drop, newest first. Listed only: never in buyers and never in any count or total.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Sum of the counted items only; an unlimited item adds nothing.
TAKEN: money that has arrived and stayed, from every paid status (paid, ready, collected, part-refunded), net of recorded refunds. Card orders without the buyer fee; cash and transfer in full.
How many boxes were put up.
How many boxes are left. Stock, never writable.
True when this drop sells one box at one price.
no_itemsno_timesno_closesno_quantityno_priceno_allergenstime_before_openstime_before_closestime_off_daycurl -X POST "https://www.membber.com/api/v1/drops" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"name": "Example name",
"opens_at": "<opens_at>",
"collection_date": "<collection_date>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
name: "Example name",
opens_at: "<opens_at>",
collection_date: "<collection_date>"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.createBatchDrop(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
name: "Example name",
opensAt: "<opens_at>",
collectionDate: "<collection_date>"
))
).ok.body.json
print(response){
"drop": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"code": "EXAMPLE10",
"shop_url": "https://example.com/image.jpg",
"image_url": "https://example.com/image.jpg",
"share_url": "https://example.com/image.jpg",
"name": "Example name",
"description": "Added at the front desk",
"status": "draft",
"state": "draft",
"opens_at": "<opens_at>",
"closes_at": "<closes_at>",
"collection_date": "<collection_date>",
"timezone": "Europe/London",
"checkout_ttl_seconds": 1,
"menu_visible_before_open": true,
"show_stock_count": true,
"max_per_customer": 1,
"items": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"item_name": "<item_name>",
"size_label": "<size_label>",
"colour_label": "<colour_label>",
"size_name": "<size_name>",
"colour_name": "<colour_name>",
"colour_photo_url": "https://example.com/image.jpg",
"price_pence": 1500,
"usual_price_pence": 1500,
"drop_price_pence": 1500,
"buyer_price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"per_bundle": -9007199254740991,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true,
"is_food": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1,
"booked_count": 1,
"booked_names": [
"<booked_name>"
],
"removable": true,
"in_dispute": true
}
],
"moves_not_reached": {
"not_told": -9007199254740991,
"not_told_names": [
"<not_told_name>"
]
},
"buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"cancelled_buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"still_to_come_pence": 1500,
"unpaid_orders_count": 1,
"bundle_price_pence": 1500,
"bundle_buyer_price_pence": 1500,
"bundle_total": 1,
"bundle_remaining": 1,
"is_bundle": true,
"blockers": [
"<blocker>"
],
"blocker_codes": [
{
"code": "no_items",
"names": [
"Example name"
]
}
],
"making_started_at": "<making_started_at>",
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
}/api/v1/drops/{dropId}Scoped to the calling store. Refused with a reason when the drop has paid orders or reviews (cancel those instead), or while somebody has its stock in a basket right now.
Supplied by the URL PATH.
curl -X DELETE "https://www.membber.com/api/v1/drops/b11f386a-0000-4000-8000-d0c5000000b1" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.DELETE("/api/v1/drops/{dropId}", {
params: { path: { dropId: "b11f386a-0000-4000-8000-d0c5000000b1" } },
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.deleteDrop(
path: .init(dropId: "b11f386a-0000-4000-8000-d0c5000000b1")
).ok.body.json
print(response){
"deleted": true,
"code": "EXAMPLE10",
"name": "Example name"
}The drop with its items, windows, live counters and anything blocking it going live.
The short code that identifies this drop.
The shop's permanent page, at its own address: https://{handle}.membber.com/
The link to this specific drop.
draftscheduledcancelledComputed at read time. Never sent by a client.
draftscheduledliveclosedcollectedcancelledNull only while a draft has no closing time yet. A scheduled drop always has one.
Copied from the menu item at read time, never stored twice. On a line that sells a size or colour: the item’s name with them, "Hoodie, M, Black".
Her item’s own name, on a line that sells one of its sizes or colours (`name` then reads "Hoodie, M, Black").
The size this line sells, as her item names it. Absent: no size.
The colour this line sells, as her item names it. Absent: no colour.
Her own word for this item’s sizes ("Flavour"), when she chose one. Absent: the word for size.
Her own word for this item’s colours ("Grind"), when she chose one. Absent: the word for colour.
The photo she chose for this colour, one of the item’s own photos. Absent: none chosen.
What one sells at in this drop, before Membber's fee: the drop's own price when it has one, else the menu price. What checkout charges.
Her usual (menu) price for one.
This drop's own price for one, or null when it sells at the usual price.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
How many of this item go in one box of a one-price drop. Null means one.
Always null for an unlimited item.
False for an item that is not food (clothes, flowers): no allergen answer is asked of it. True or absent: food.
Send it back on the windows PUT to edit this window in place.
null = no cap on orders in this window.
People still to collect from this window. Moving it needs confirm_booked_changes; removing it is refused.
First names of the people still to collect, oldest order first.
False when anyone holds a place in this window, collected or not: removing it is refused.
True when the only orders holding this window are disputed ones nobody collected. Absent otherwise.
Buyers still to collect whom a moved collection time could not reach. Absent when there are none.
How many buyers could not be reached about a moved time.
Their first names, oldest order first, three at most (the most the sentence names). Shorter than not_told when one gave no name or there are more than three; absent when none gave one.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Orders refunded in full, disputed by the buyer, released unpaid or cancelled with the drop, newest first. Listed only: never in buyers and never in any count or total.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Sum of the counted items only; an unlimited item adds nothing.
TAKEN: money that has arrived and stayed, from every paid status (paid, ready, collected, part-refunded), net of recorded refunds. Card orders without the buyer fee; cash and transfer in full.
How many boxes were put up.
How many boxes are left. Stock, never writable.
True when this drop sells one box at one price.
no_itemsno_timesno_closesno_quantityno_priceno_allergenstime_before_openstime_before_closestime_off_daycurl -G "https://www.membber.com/api/v1/drops/b11f386a-0000-4000-8000-d0c5000000b1" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/{dropId}", {
params: { path: { dropId: "b11f386a-0000-4000-8000-d0c5000000b1" }, query: { store_id: "6659c139-0000-4000-8000-d0c500000066" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getBatchDrop(
path: .init(dropId: "b11f386a-0000-4000-8000-d0c5000000b1"),
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"drop": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"code": "EXAMPLE10",
"shop_url": "https://example.com/image.jpg",
"image_url": "https://example.com/image.jpg",
"share_url": "https://example.com/image.jpg",
"name": "Example name",
"description": "Added at the front desk",
"status": "draft",
"state": "draft",
"opens_at": "<opens_at>",
"closes_at": "<closes_at>",
"collection_date": "<collection_date>",
"timezone": "Europe/London",
"checkout_ttl_seconds": 1,
"menu_visible_before_open": true,
"show_stock_count": true,
"max_per_customer": 1,
"items": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"item_name": "<item_name>",
"size_label": "<size_label>",
"colour_label": "<colour_label>",
"size_name": "<size_name>",
"colour_name": "<colour_name>",
"colour_photo_url": "https://example.com/image.jpg",
"price_pence": 1500,
"usual_price_pence": 1500,
"drop_price_pence": 1500,
"buyer_price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"per_bundle": -9007199254740991,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true,
"is_food": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1,
"booked_count": 1,
"booked_names": [
"<booked_name>"
],
"removable": true,
"in_dispute": true
}
],
"moves_not_reached": {
"not_told": -9007199254740991,
"not_told_names": [
"<not_told_name>"
]
},
"buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"cancelled_buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"still_to_come_pence": 1500,
"unpaid_orders_count": 1,
"bundle_price_pence": 1500,
"bundle_buyer_price_pence": 1500,
"bundle_total": 1,
"bundle_remaining": 1,
"is_bundle": true,
"blockers": [
"<blocker>"
],
"blocker_codes": [
{
"code": "no_items",
"names": [
"Example name"
]
}
],
"making_started_at": "<making_started_at>",
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
}Edits a drop, including one that is currently selling: adding stock, reordering items, adding a window or closing early are all expected mid-drop. Moving `status` from draft to scheduled is what makes it go live at its opening minute; that is refused while anything is still missing (an item's allergens or price, a collection window, the closing time) and the refusal says what. The check reads the drop as this PATCH leaves it, so the closing time may arrive in the same request as the status. A new `collection_date` moves every collection window to that day in the same write (clock times kept); if people booked one, it is refused with 409 BOOKED_WINDOWS_AFFECTED until sent with confirm_booked_changes.
draftscheduledISO instant. The drop opens itself at this minute.
ISO instant. Null is allowed on a DRAFT (nothing is on sale yet, so nothing closes), but a drop cannot be scheduled without a closing time. It may be sent in the same PATCH as status=scheduled.
YYYY-MM-DD in the store timezone.
What the whole box costs, in pence. Null means this is not a box drop.
How many boxes exist. Null means this is not a box drop.
Basket hold, in seconds. Defaults to 300 (5 minutes).
null = no limit.
True once the seller has agreed to move collection times people booked. Without it such a change is refused with 409 BOOKED_WINDOWS_AFFECTED naming them. Each affected buyer is told once.
The short code that identifies this drop.
The shop's permanent page, at its own address: https://{handle}.membber.com/
The link to this specific drop.
draftscheduledcancelledComputed at read time. Never sent by a client.
draftscheduledliveclosedcollectedcancelledNull only while a draft has no closing time yet. A scheduled drop always has one.
Copied from the menu item at read time, never stored twice. On a line that sells a size or colour: the item’s name with them, "Hoodie, M, Black".
Her item’s own name, on a line that sells one of its sizes or colours (`name` then reads "Hoodie, M, Black").
The size this line sells, as her item names it. Absent: no size.
The colour this line sells, as her item names it. Absent: no colour.
Her own word for this item’s sizes ("Flavour"), when she chose one. Absent: the word for size.
Her own word for this item’s colours ("Grind"), when she chose one. Absent: the word for colour.
The photo she chose for this colour, one of the item’s own photos. Absent: none chosen.
What one sells at in this drop, before Membber's fee: the drop's own price when it has one, else the menu price. What checkout charges.
Her usual (menu) price for one.
This drop's own price for one, or null when it sells at the usual price.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
How many of this item go in one box of a one-price drop. Null means one.
Always null for an unlimited item.
False for an item that is not food (clothes, flowers): no allergen answer is asked of it. True or absent: food.
Send it back on the windows PUT to edit this window in place.
null = no cap on orders in this window.
People still to collect from this window. Moving it needs confirm_booked_changes; removing it is refused.
First names of the people still to collect, oldest order first.
False when anyone holds a place in this window, collected or not: removing it is refused.
True when the only orders holding this window are disputed ones nobody collected. Absent otherwise.
Buyers still to collect whom a moved collection time could not reach. Absent when there are none.
How many buyers could not be reached about a moved time.
Their first names, oldest order first, three at most (the most the sentence names). Shorter than not_told when one gave no name or there are more than three; absent when none gave one.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Orders refunded in full, disputed by the buyer, released unpaid or cancelled with the drop, newest first. Listed only: never in buyers and never in any count or total.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Sum of the counted items only; an unlimited item adds nothing.
TAKEN: money that has arrived and stayed, from every paid status (paid, ready, collected, part-refunded), net of recorded refunds. Card orders without the buyer fee; cash and transfer in full.
How many boxes were put up.
How many boxes are left. Stock, never writable.
True when this drop sells one box at one price.
no_itemsno_timesno_closesno_quantityno_priceno_allergenstime_before_openstime_before_closestime_off_dayWho a move of booked collection times reached.
Orders whose collection time moved with their window.
Of those, buyers told the new time by text or email.
Buyers who could not be told (no mobile or email that worked). The seller should reach them. A buyer whose message is still waiting to be sent is in neither count.
First names of the buyers who could not be told, oldest order first, as given on their orders at this shop (the names `windows[].booked_names` gave before the move), three at most: the most the sentence names, which from four says the first two and counts the rest from `not_told`. A buyer who gave no name is not in it, so it can be shorter than `not_told`. Absent when none of them gave one: say the count.
curl -X PATCH "https://www.membber.com/api/v1/drops/b11f386a-0000-4000-8000-d0c5000000b1" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PATCH("/api/v1/drops/{dropId}", {
params: { path: { dropId: "b11f386a-0000-4000-8000-d0c5000000b1" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.updateBatchDrop(
path: .init(dropId: "b11f386a-0000-4000-8000-d0c5000000b1"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"drop": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"code": "EXAMPLE10",
"shop_url": "https://example.com/image.jpg",
"image_url": "https://example.com/image.jpg",
"share_url": "https://example.com/image.jpg",
"name": "Example name",
"description": "Added at the front desk",
"status": "draft",
"state": "draft",
"opens_at": "<opens_at>",
"closes_at": "<closes_at>",
"collection_date": "<collection_date>",
"timezone": "Europe/London",
"checkout_ttl_seconds": 1,
"menu_visible_before_open": true,
"show_stock_count": true,
"max_per_customer": 1,
"items": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"item_name": "<item_name>",
"size_label": "<size_label>",
"colour_label": "<colour_label>",
"size_name": "<size_name>",
"colour_name": "<colour_name>",
"colour_photo_url": "https://example.com/image.jpg",
"price_pence": 1500,
"usual_price_pence": 1500,
"drop_price_pence": 1500,
"buyer_price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"per_bundle": -9007199254740991,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true,
"is_food": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1,
"booked_count": 1,
"booked_names": [
"<booked_name>"
],
"removable": true,
"in_dispute": true
}
],
"moves_not_reached": {
"not_told": -9007199254740991,
"not_told_names": [
"<not_told_name>"
]
},
"buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"cancelled_buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"still_to_come_pence": 1500,
"unpaid_orders_count": 1,
"bundle_price_pence": 1500,
"bundle_buyer_price_pence": 1500,
"bundle_total": 1,
"bundle_remaining": 1,
"is_bundle": true,
"blockers": [
"<blocker>"
],
"blocker_codes": [
{
"code": "no_items",
"names": [
"Example name"
]
}
],
"making_started_at": "<making_started_at>",
"created_at": "<created_at>",
"updated_at": "<updated_at>"
},
"moved_bookings": {
"orders": -9007199254740991,
"told": -9007199254740991,
"not_told": -9007199254740991,
"not_told_names": [
"<not_told_name>"
]
}
}Cancels the drop. Any paid order is refunded and every affected person is told once, however many orders they hold. A drop with paid orders requires `confirm_refunds: true`, so nobody cancels fifty people without being shown the number first. An order already collected, or marked not collected, is neither refunded nor told. `preview: true` answers what it would do and changes nothing. Requires the refund permission.
True: only say what calling the drop off would do (the same numbers and sentence), and change nothing, whatever else is sent.
The short code that identifies this drop.
The shop's permanent page, at its own address: https://{handle}.membber.com/
The link to this specific drop.
draftscheduledcancelledComputed at read time. Never sent by a client.
draftscheduledliveclosedcollectedcancelledNull only while a draft has no closing time yet. A scheduled drop always has one.
Copied from the menu item at read time, never stored twice. On a line that sells a size or colour: the item’s name with them, "Hoodie, M, Black".
Her item’s own name, on a line that sells one of its sizes or colours (`name` then reads "Hoodie, M, Black").
The size this line sells, as her item names it. Absent: no size.
The colour this line sells, as her item names it. Absent: no colour.
Her own word for this item’s sizes ("Flavour"), when she chose one. Absent: the word for size.
Her own word for this item’s colours ("Grind"), when she chose one. Absent: the word for colour.
The photo she chose for this colour, one of the item’s own photos. Absent: none chosen.
What one sells at in this drop, before Membber's fee: the drop's own price when it has one, else the menu price. What checkout charges.
Her usual (menu) price for one.
This drop's own price for one, or null when it sells at the usual price.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
How many of this item go in one box of a one-price drop. Null means one.
Always null for an unlimited item.
False for an item that is not food (clothes, flowers): no allergen answer is asked of it. True or absent: food.
Send it back on the windows PUT to edit this window in place.
null = no cap on orders in this window.
People still to collect from this window. Moving it needs confirm_booked_changes; removing it is refused.
First names of the people still to collect, oldest order first.
False when anyone holds a place in this window, collected or not: removing it is refused.
True when the only orders holding this window are disputed ones nobody collected. Absent otherwise.
Buyers still to collect whom a moved collection time could not reach. Absent when there are none.
How many buyers could not be reached about a moved time.
Their first names, oldest order first, three at most (the most the sentence names). Shorter than not_told when one gave no name or there are more than three; absent when none gave one.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Orders refunded in full, disputed by the buyer, released unpaid or cancelled with the drop, newest first. Listed only: never in buyers and never in any count or total.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Sum of the counted items only; an unlimited item adds nothing.
TAKEN: money that has arrived and stayed, from every paid status (paid, ready, collected, part-refunded), net of recorded refunds. Card orders without the buyer fee; cash and transfer in full.
How many boxes were put up.
How many boxes are left. Stock, never writable.
True when this drop sells one box at one price.
no_itemsno_timesno_closesno_quantityno_priceno_allergenstime_before_openstime_before_closestime_off_dayHow many orders will be refunded.
How many PEOPLE, which is fewer if anyone ordered twice.
True on the answer to a preview: nothing was changed.
On a preview: who is refunded and who is told, in the sentence the merchant confirms. Empty when nobody is owed anything or told.
curl -X POST "https://www.membber.com/api/v1/drops/b11f386a-0000-4000-8000-d0c5000000b1/cancel" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/{dropId}/cancel", {
params: { path: { dropId: "b11f386a-0000-4000-8000-d0c5000000b1" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.cancelBatchDrop(
path: .init(dropId: "b11f386a-0000-4000-8000-d0c5000000b1"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"drop": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"code": "EXAMPLE10",
"shop_url": "https://example.com/image.jpg",
"image_url": "https://example.com/image.jpg",
"share_url": "https://example.com/image.jpg",
"name": "Example name",
"description": "Added at the front desk",
"status": "draft",
"state": "draft",
"opens_at": "<opens_at>",
"closes_at": "<closes_at>",
"collection_date": "<collection_date>",
"timezone": "Europe/London",
"checkout_ttl_seconds": 1,
"menu_visible_before_open": true,
"show_stock_count": true,
"max_per_customer": 1,
"items": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"item_name": "<item_name>",
"size_label": "<size_label>",
"colour_label": "<colour_label>",
"size_name": "<size_name>",
"colour_name": "<colour_name>",
"colour_photo_url": "https://example.com/image.jpg",
"price_pence": 1500,
"usual_price_pence": 1500,
"drop_price_pence": 1500,
"buyer_price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"per_bundle": -9007199254740991,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true,
"is_food": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1,
"booked_count": 1,
"booked_names": [
"<booked_name>"
],
"removable": true,
"in_dispute": true
}
],
"moves_not_reached": {
"not_told": -9007199254740991,
"not_told_names": [
"<not_told_name>"
]
},
"buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"cancelled_buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"still_to_come_pence": 1500,
"unpaid_orders_count": 1,
"bundle_price_pence": 1500,
"bundle_buyer_price_pence": 1500,
"bundle_total": 1,
"bundle_remaining": 1,
"is_bundle": true,
"blockers": [
"<blocker>"
],
"blocker_codes": [
{
"code": "no_items",
"names": [
"Example name"
]
}
],
"making_started_at": "<making_started_at>",
"created_at": "<created_at>",
"updated_at": "<updated_at>"
},
"orders_to_refund": 1,
"people_to_tell": 1,
"refund_pence": 1500,
"preview": true,
"message": "Added at the front desk"
}Copies a drop's name, description, photo, items and quantities, collection windows and settings onto a new date, as a DRAFT. With no dates supplied it moves forward in whole weeks from the original, keeping the weekday and the time of day. Orders, sales and announcements are never copied, and the original keeps its link.
Send the same token on a retry to get the SAME drop back instead of a duplicate.
Defaults to the source's closing time, moved with the date. Null leaves the copy without one: it is a draft, and cannot be scheduled until a closing time is set.
The short code that identifies this drop.
The shop's permanent page, at its own address: https://{handle}.membber.com/
The link to this specific drop.
draftscheduledcancelledComputed at read time. Never sent by a client.
draftscheduledliveclosedcollectedcancelledNull only while a draft has no closing time yet. A scheduled drop always has one.
Copied from the menu item at read time, never stored twice. On a line that sells a size or colour: the item’s name with them, "Hoodie, M, Black".
Her item’s own name, on a line that sells one of its sizes or colours (`name` then reads "Hoodie, M, Black").
The size this line sells, as her item names it. Absent: no size.
The colour this line sells, as her item names it. Absent: no colour.
Her own word for this item’s sizes ("Flavour"), when she chose one. Absent: the word for size.
Her own word for this item’s colours ("Grind"), when she chose one. Absent: the word for colour.
The photo she chose for this colour, one of the item’s own photos. Absent: none chosen.
What one sells at in this drop, before Membber's fee: the drop's own price when it has one, else the menu price. What checkout charges.
Her usual (menu) price for one.
This drop's own price for one, or null when it sells at the usual price.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
How many of this item go in one box of a one-price drop. Null means one.
Always null for an unlimited item.
False for an item that is not food (clothes, flowers): no allergen answer is asked of it. True or absent: food.
Send it back on the windows PUT to edit this window in place.
null = no cap on orders in this window.
People still to collect from this window. Moving it needs confirm_booked_changes; removing it is refused.
First names of the people still to collect, oldest order first.
False when anyone holds a place in this window, collected or not: removing it is refused.
True when the only orders holding this window are disputed ones nobody collected. Absent otherwise.
Buyers still to collect whom a moved collection time could not reach. Absent when there are none.
How many buyers could not be reached about a moved time.
Their first names, oldest order first, three at most (the most the sentence names). Shorter than not_told when one gave no name or there are more than three; absent when none gave one.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Orders refunded in full, disputed by the buyer, released unpaid or cancelled with the drop, newest first. Listed only: never in buyers and never in any count or total.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Sum of the counted items only; an unlimited item adds nothing.
TAKEN: money that has arrived and stayed, from every paid status (paid, ready, collected, part-refunded), net of recorded refunds. Card orders without the buyer fee; cash and transfer in full.
How many boxes were put up.
How many boxes are left. Stock, never writable.
True when this drop sells one box at one price.
no_itemsno_timesno_closesno_quantityno_priceno_allergenstime_before_openstime_before_closestime_off_daycurl -X POST "https://www.membber.com/api/v1/drops/b11f386a-0000-4000-8000-d0c5000000b1/duplicate" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/{dropId}/duplicate", {
params: { path: { dropId: "b11f386a-0000-4000-8000-d0c5000000b1" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.duplicateBatchDrop(
path: .init(dropId: "b11f386a-0000-4000-8000-d0c5000000b1"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"drop": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"code": "EXAMPLE10",
"shop_url": "https://example.com/image.jpg",
"image_url": "https://example.com/image.jpg",
"share_url": "https://example.com/image.jpg",
"name": "Example name",
"description": "Added at the front desk",
"status": "draft",
"state": "draft",
"opens_at": "<opens_at>",
"closes_at": "<closes_at>",
"collection_date": "<collection_date>",
"timezone": "Europe/London",
"checkout_ttl_seconds": 1,
"menu_visible_before_open": true,
"show_stock_count": true,
"max_per_customer": 1,
"items": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"item_name": "<item_name>",
"size_label": "<size_label>",
"colour_label": "<colour_label>",
"size_name": "<size_name>",
"colour_name": "<colour_name>",
"colour_photo_url": "https://example.com/image.jpg",
"price_pence": 1500,
"usual_price_pence": 1500,
"drop_price_pence": 1500,
"buyer_price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"per_bundle": -9007199254740991,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true,
"is_food": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1,
"booked_count": 1,
"booked_names": [
"<booked_name>"
],
"removable": true,
"in_dispute": true
}
],
"moves_not_reached": {
"not_told": -9007199254740991,
"not_told_names": [
"<not_told_name>"
]
},
"buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"cancelled_buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"still_to_come_pence": 1500,
"unpaid_orders_count": 1,
"bundle_price_pence": 1500,
"bundle_buyer_price_pence": 1500,
"bundle_total": 1,
"bundle_remaining": 1,
"is_bundle": true,
"blockers": [
"<blocker>"
],
"blocker_codes": [
{
"code": "no_items",
"names": [
"Example name"
]
}
],
"making_started_at": "<making_started_at>",
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
}Replaces the drop menu with the given items, chosen from the store menu that already exists. Raising a quantity mid-drop puts stock back on sale immediately. Lowering it can never go below what is already sold, and the refusal says how many that is. An item omitted here is removed from the drop, which is refused if any of it has sold. In a one-price box drop, per_bundle says how many of an item go in one box; an item already in the drop keeps that count once the box is selling or any of it has sold. An item with sizes or colours sells as one line per pair (size_label, colour_label), each with its own quantity and drop_price_pence, and one max_per_order for all of its lines; a box has none.
The size this line sells, as her item names it ("" or absent: none). One line per size and colour.
The colour this line sells, as her item names it ("" or absent: none). One line per size and colour.
This drop's own price for one, in pence. Leave it out to keep the saved price; null for the usual price.
How many of this item go in one box of a one-price drop. Leave it out to keep what is saved; null for one.
The short code that identifies this drop.
The shop's permanent page, at its own address: https://{handle}.membber.com/
The link to this specific drop.
draftscheduledcancelledComputed at read time. Never sent by a client.
draftscheduledliveclosedcollectedcancelledNull only while a draft has no closing time yet. A scheduled drop always has one.
Copied from the menu item at read time, never stored twice. On a line that sells a size or colour: the item’s name with them, "Hoodie, M, Black".
Her item’s own name, on a line that sells one of its sizes or colours (`name` then reads "Hoodie, M, Black").
The size this line sells, as her item names it. Absent: no size.
The colour this line sells, as her item names it. Absent: no colour.
Her own word for this item’s sizes ("Flavour"), when she chose one. Absent: the word for size.
Her own word for this item’s colours ("Grind"), when she chose one. Absent: the word for colour.
The photo she chose for this colour, one of the item’s own photos. Absent: none chosen.
What one sells at in this drop, before Membber's fee: the drop's own price when it has one, else the menu price. What checkout charges.
Her usual (menu) price for one.
This drop's own price for one, or null when it sells at the usual price.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
How many of this item go in one box of a one-price drop. Null means one.
Always null for an unlimited item.
False for an item that is not food (clothes, flowers): no allergen answer is asked of it. True or absent: food.
Send it back on the windows PUT to edit this window in place.
null = no cap on orders in this window.
People still to collect from this window. Moving it needs confirm_booked_changes; removing it is refused.
First names of the people still to collect, oldest order first.
False when anyone holds a place in this window, collected or not: removing it is refused.
True when the only orders holding this window are disputed ones nobody collected. Absent otherwise.
Buyers still to collect whom a moved collection time could not reach. Absent when there are none.
How many buyers could not be reached about a moved time.
Their first names, oldest order first, three at most (the most the sentence names). Shorter than not_told when one gave no name or there are more than three; absent when none gave one.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Orders refunded in full, disputed by the buyer, released unpaid or cancelled with the drop, newest first. Listed only: never in buyers and never in any count or total.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Sum of the counted items only; an unlimited item adds nothing.
TAKEN: money that has arrived and stayed, from every paid status (paid, ready, collected, part-refunded), net of recorded refunds. Card orders without the buyer fee; cash and transfer in full.
How many boxes were put up.
How many boxes are left. Stock, never writable.
True when this drop sells one box at one price.
no_itemsno_timesno_closesno_quantityno_priceno_allergenstime_before_openstime_before_closestime_off_daycurl -X PUT "https://www.membber.com/api/v1/drops/b11f386a-0000-4000-8000-d0c5000000b1/items" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"items": [
{
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"quantity": 1
}
]
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PUT("/api/v1/drops/{dropId}/items", {
params: { path: { dropId: "b11f386a-0000-4000-8000-d0c5000000b1" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
items: [
{
menu_item_id: "ca65a6e7-0000-4000-8000-d0c5000000ca",
quantity: 1
}
]
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.setBatchDropItems(
path: .init(dropId: "b11f386a-0000-4000-8000-d0c5000000b1"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
items: [.init(
menuItemId: "ca65a6e7-0000-4000-8000-d0c5000000ca",
quantity: 1
)]
))
).ok.body.json
print(response){
"drop": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"code": "EXAMPLE10",
"shop_url": "https://example.com/image.jpg",
"image_url": "https://example.com/image.jpg",
"share_url": "https://example.com/image.jpg",
"name": "Example name",
"description": "Added at the front desk",
"status": "draft",
"state": "draft",
"opens_at": "<opens_at>",
"closes_at": "<closes_at>",
"collection_date": "<collection_date>",
"timezone": "Europe/London",
"checkout_ttl_seconds": 1,
"menu_visible_before_open": true,
"show_stock_count": true,
"max_per_customer": 1,
"items": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"item_name": "<item_name>",
"size_label": "<size_label>",
"colour_label": "<colour_label>",
"size_name": "<size_name>",
"colour_name": "<colour_name>",
"colour_photo_url": "https://example.com/image.jpg",
"price_pence": 1500,
"usual_price_pence": 1500,
"drop_price_pence": 1500,
"buyer_price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"per_bundle": -9007199254740991,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true,
"is_food": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1,
"booked_count": 1,
"booked_names": [
"<booked_name>"
],
"removable": true,
"in_dispute": true
}
],
"moves_not_reached": {
"not_told": -9007199254740991,
"not_told_names": [
"<not_told_name>"
]
},
"buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"cancelled_buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"still_to_come_pence": 1500,
"unpaid_orders_count": 1,
"bundle_price_pence": 1500,
"bundle_buyer_price_pence": 1500,
"bundle_total": 1,
"bundle_remaining": 1,
"is_bundle": true,
"blockers": [
"<blocker>"
],
"blocker_codes": [
{
"code": "no_items",
"names": [
"Example name"
]
}
],
"making_started_at": "<making_started_at>",
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
}The shop adds an order somebody asked for outside Membber (a DM, a call). Only while the drop is selling and the shop takes cash when they collect. The boxes and the window are taken exactly as a pay-later order takes them. Paid to the shop, never by card: `paid: false` waits for payment at collection, `paid: true` records it paid now. No 5% fee. The person is never texted, emailed, linked to a customer or put on any list. Idempotent on `client_token`. Requires the payments permission.
One per sheet opening, kept across retries: the same token never adds a second order.
Their first and last name.
Their mobile: 07… or +… Never messaged.
One of this drop's collection windows.
An item drop: this drop's items (`batch_drop_items.id`) and how many.
A box drop: how many boxes, instead of `lines`.
false: Pay when they collect. true: Already paid.
awaiting_paymentpaidThe goods alone: a hand-added order carries no 5%.
True when this token had already added the order; nothing new was added.
curl -X POST "https://www.membber.com/api/v1/drops/b11f386a-0000-4000-8000-d0c5000000b1/orders" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"client_token": "bf14cc85-0000-4000-8000-d0c5000000bf",
"name": "Example name",
"phone": "+44 7700 900123",
"window_id": "f310cfca-0000-4000-8000-d0c5000000f3",
"paid": true
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/{dropId}/orders", {
params: { path: { dropId: "b11f386a-0000-4000-8000-d0c5000000b1" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
client_token: "bf14cc85-0000-4000-8000-d0c5000000bf",
name: "Example name",
phone: "+44 7700 900123",
window_id: "f310cfca-0000-4000-8000-d0c5000000f3",
paid: true
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.addDropOrder(
path: .init(dropId: "b11f386a-0000-4000-8000-d0c5000000b1"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
clientToken: "bf14cc85-0000-4000-8000-d0c5000000bf",
name: "Example name",
phone: "+44 7700 900123",
windowId: "f310cfca-0000-4000-8000-d0c5000000f3",
paid: true
))
).ok.body.json
print(response){
"order_id": "4991ffac-0000-4000-8000-d0c500000049",
"order_number": "<order_number>",
"status": "awaiting_payment",
"total_pence": 1500,
"collection_code": "EXAMPLE10",
"replayed": true
}What to make (each item’s total, split paid and not yet paid, with a box multiplied out into its items) and who is coming in each collection window (buyer, what they bought, collection code, payment). Read from the same orders the drop board counts, so a released, cancelled or refunded order is never on it. Read-only.
draftscheduledliveclosedcollectedcancelledWhen this sheet was read. Printed on it, so an old printout says it is old.
One line per thing to make, in the drop's own item order. A box is multiplied out into what goes in it.
Everything to make for this item: paid plus not yet paid.
Every collection window in time order, each with its orders by buyer name.
Null for the orders no collection window could be found for.
The name the buyer gave. Null when they gave none.
`paid` once the money is recorded; `unpaid` for a placed bank-transfer or cash order.
paidunpaidTrue once the shop marked the order "didn't come" after its collection window. It stays on the sheet (its boxes were made) and is never counted as not yet paid.
The code the buyer shows at the counter. Null while it is held (an unpaid bank transfer) or when the order has none.
True for an unpaid bank transfer: the buyer has not been sent a code yet, so none is printed.
What the buyer bought, as they bought it. A box stays one line here.
FA247: the shop added this order by hand. Printed as "Added by you".
Orders the drop board still counts that are not on this sheet, and why.
curl -G "https://www.membber.com/api/v1/drops/b11f386a-0000-4000-8000-d0c5000000b1/prep" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/{dropId}/prep", {
params: { path: { dropId: "b11f386a-0000-4000-8000-d0c5000000b1" }, query: { store_id: "6659c139-0000-4000-8000-d0c500000066" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getBatchDropPrep(
path: .init(dropId: "b11f386a-0000-4000-8000-d0c5000000b1"),
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"prep": {
"drop": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"state": "draft",
"collection_date": "<collection_date>",
"timezone": "Europe/London",
"is_bundle": true
},
"generated_at": "<generated_at>",
"to_make": [
{
"key": "<key>",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"total": -9007199254740991,
"paid": -9007199254740991,
"unpaid": -9007199254740991
}
],
"totals": {
"orders": -9007199254740991,
"paid_orders": -9007199254740991,
"unpaid_orders": -9007199254740991,
"collected_orders": -9007199254740991,
"units": -9007199254740991,
"unpaid_units": -9007199254740991
},
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"orders": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"payment": "paid",
"payment_method_kind": "<payment_method_kind>",
"collected": true,
"didnt_come": true,
"collection_code": "EXAMPLE10",
"code_held_until_paid": true,
"total_pence": 1500,
"lines": [
null
],
"added_by_shop": true
}
]
}
],
"left_off": {
"collection_cancelled": -9007199254740991
}
}
}Records that the merchant has started making this drop and queues one message to every buyer on it. OPTIONAL: a drop nobody starts behaves exactly as it does today, and this is never a step anyone must complete. Idempotent, a second call returns `already_started: true` and sends nothing, so a double tap on a flaky connection cannot text everyone twice. Messages are DELIVERED by the sweep rather than by this call, inside the shop’s sending hours, because a baker who starts at 04:40 must not text forty strangers at 04:40. Never carries the collection address or code: the reminder two hours before the window is the message that tells people what to do, and this one is early on purpose.
The short code that identifies this drop.
The shop's permanent page, at its own address: https://{handle}.membber.com/
The link to this specific drop.
draftscheduledcancelledComputed at read time. Never sent by a client.
draftscheduledliveclosedcollectedcancelledNull only while a draft has no closing time yet. A scheduled drop always has one.
Copied from the menu item at read time, never stored twice. On a line that sells a size or colour: the item’s name with them, "Hoodie, M, Black".
Her item’s own name, on a line that sells one of its sizes or colours (`name` then reads "Hoodie, M, Black").
The size this line sells, as her item names it. Absent: no size.
The colour this line sells, as her item names it. Absent: no colour.
Her own word for this item’s sizes ("Flavour"), when she chose one. Absent: the word for size.
Her own word for this item’s colours ("Grind"), when she chose one. Absent: the word for colour.
The photo she chose for this colour, one of the item’s own photos. Absent: none chosen.
What one sells at in this drop, before Membber's fee: the drop's own price when it has one, else the menu price. What checkout charges.
Her usual (menu) price for one.
This drop's own price for one, or null when it sells at the usual price.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
How many of this item go in one box of a one-price drop. Null means one.
Always null for an unlimited item.
False for an item that is not food (clothes, flowers): no allergen answer is asked of it. True or absent: food.
Send it back on the windows PUT to edit this window in place.
null = no cap on orders in this window.
People still to collect from this window. Moving it needs confirm_booked_changes; removing it is refused.
First names of the people still to collect, oldest order first.
False when anyone holds a place in this window, collected or not: removing it is refused.
True when the only orders holding this window are disputed ones nobody collected. Absent otherwise.
Buyers still to collect whom a moved collection time could not reach. Absent when there are none.
How many buyers could not be reached about a moved time.
Their first names, oldest order first, three at most (the most the sentence names). Shorter than not_told when one gave no name or there are more than three; absent when none gave one.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Orders refunded in full, disputed by the buyer, released unpaid or cancelled with the drop, newest first. Listed only: never in buyers and never in any count or total.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Sum of the counted items only; an unlimited item adds nothing.
TAKEN: money that has arrived and stayed, from every paid status (paid, ready, collected, part-refunded), net of recorded refunds. Card orders without the buyer fee; cash and transfer in full.
How many boxes were put up.
How many boxes are left. Stock, never writable.
True when this drop sells one box at one price.
no_itemsno_timesno_closesno_quantityno_priceno_allergenstime_before_openstime_before_closestime_off_dayHow many orders are queued to hear, so the app can say "23 people will be told" rather than a vague success.
True when this drop had already been started. The call is a safe no-op and nobody is told twice.
curl -X POST "https://www.membber.com/api/v1/drops/b11f386a-0000-4000-8000-d0c5000000b1/started" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/{dropId}/started", {
params: { path: { dropId: "b11f386a-0000-4000-8000-d0c5000000b1" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.startBatchDropMaking(
path: .init(dropId: "b11f386a-0000-4000-8000-d0c5000000b1"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"drop": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"code": "EXAMPLE10",
"shop_url": "https://example.com/image.jpg",
"image_url": "https://example.com/image.jpg",
"share_url": "https://example.com/image.jpg",
"name": "Example name",
"description": "Added at the front desk",
"status": "draft",
"state": "draft",
"opens_at": "<opens_at>",
"closes_at": "<closes_at>",
"collection_date": "<collection_date>",
"timezone": "Europe/London",
"checkout_ttl_seconds": 1,
"menu_visible_before_open": true,
"show_stock_count": true,
"max_per_customer": 1,
"items": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"item_name": "<item_name>",
"size_label": "<size_label>",
"colour_label": "<colour_label>",
"size_name": "<size_name>",
"colour_name": "<colour_name>",
"colour_photo_url": "https://example.com/image.jpg",
"price_pence": 1500,
"usual_price_pence": 1500,
"drop_price_pence": 1500,
"buyer_price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"per_bundle": -9007199254740991,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true,
"is_food": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1,
"booked_count": 1,
"booked_names": [
"<booked_name>"
],
"removable": true,
"in_dispute": true
}
],
"moves_not_reached": {
"not_told": -9007199254740991,
"not_told_names": [
"<not_told_name>"
]
},
"buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"cancelled_buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"still_to_come_pence": 1500,
"unpaid_orders_count": 1,
"bundle_price_pence": 1500,
"bundle_buyer_price_pence": 1500,
"bundle_total": 1,
"bundle_remaining": 1,
"is_bundle": true,
"blockers": [
"<blocker>"
],
"blocker_codes": [
{
"code": "no_items",
"names": [
"Example name"
]
}
],
"making_started_at": "<making_started_at>",
"created_at": "<created_at>",
"updated_at": "<updated_at>"
},
"people_to_tell": 1,
"already_started": true
}Replaces the drop collection windows. A drop owns its own windows for ONE day, its `collection_date`: a window on any other day is refused. Send each window's `id` to edit it in place, so the orders booked into it stay with it. Windows can be added while the drop is selling. Removing a window people booked is refused; moving one people are still coming to is refused with 409 BOOKED_WINDOWS_AFFECTED (naming them) until sent with confirm_booked_changes, which moves it and texts or emails each of them once.
The window this edits, in place (its bookings stay on it). Omit for a new window.
True once the seller has agreed to move collection times people booked. Without it such a change is refused with 409 BOOKED_WINDOWS_AFFECTED naming them. Each affected buyer is told once.
The short code that identifies this drop.
The shop's permanent page, at its own address: https://{handle}.membber.com/
The link to this specific drop.
draftscheduledcancelledComputed at read time. Never sent by a client.
draftscheduledliveclosedcollectedcancelledNull only while a draft has no closing time yet. A scheduled drop always has one.
Copied from the menu item at read time, never stored twice. On a line that sells a size or colour: the item’s name with them, "Hoodie, M, Black".
Her item’s own name, on a line that sells one of its sizes or colours (`name` then reads "Hoodie, M, Black").
The size this line sells, as her item names it. Absent: no size.
The colour this line sells, as her item names it. Absent: no colour.
Her own word for this item’s sizes ("Flavour"), when she chose one. Absent: the word for size.
Her own word for this item’s colours ("Grind"), when she chose one. Absent: the word for colour.
The photo she chose for this colour, one of the item’s own photos. Absent: none chosen.
What one sells at in this drop, before Membber's fee: the drop's own price when it has one, else the menu price. What checkout charges.
Her usual (menu) price for one.
This drop's own price for one, or null when it sells at the usual price.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
How many of this item go in one box of a one-price drop. Null means one.
Always null for an unlimited item.
False for an item that is not food (clothes, flowers): no allergen answer is asked of it. True or absent: food.
Send it back on the windows PUT to edit this window in place.
null = no cap on orders in this window.
People still to collect from this window. Moving it needs confirm_booked_changes; removing it is refused.
First names of the people still to collect, oldest order first.
False when anyone holds a place in this window, collected or not: removing it is refused.
True when the only orders holding this window are disputed ones nobody collected. Absent otherwise.
Buyers still to collect whom a moved collection time could not reach. Absent when there are none.
How many buyers could not be reached about a moved time.
Their first names, oldest order first, three at most (the most the sentence names). Shorter than not_told when one gave no name or there are more than three; absent when none gave one.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Orders refunded in full, disputed by the buyer, released unpaid or cancelled with the drop, newest first. Listed only: never in buyers and never in any count or total.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Sum of the counted items only; an unlimited item adds nothing.
TAKEN: money that has arrived and stayed, from every paid status (paid, ready, collected, part-refunded), net of recorded refunds. Card orders without the buyer fee; cash and transfer in full.
How many boxes were put up.
How many boxes are left. Stock, never writable.
True when this drop sells one box at one price.
no_itemsno_timesno_closesno_quantityno_priceno_allergenstime_before_openstime_before_closestime_off_dayWho a move of booked collection times reached.
Orders whose collection time moved with their window.
Of those, buyers told the new time by text or email.
Buyers who could not be told (no mobile or email that worked). The seller should reach them. A buyer whose message is still waiting to be sent is in neither count.
First names of the buyers who could not be told, oldest order first, as given on their orders at this shop (the names `windows[].booked_names` gave before the move), three at most: the most the sentence names, which from four says the first two and counts the rest from `not_told`. A buyer who gave no name is not in it, so it can be shorter than `not_told`. Absent when none of them gave one: say the count.
curl -X PUT "https://www.membber.com/api/v1/drops/b11f386a-0000-4000-8000-d0c5000000b1/windows" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"windows": [
{
"starts_at": "<starts_at>",
"ends_at": "<ends_at>"
}
]
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PUT("/api/v1/drops/{dropId}/windows", {
params: { path: { dropId: "b11f386a-0000-4000-8000-d0c5000000b1" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
windows: [
{
starts_at: "<starts_at>",
ends_at: "<ends_at>"
}
]
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.setBatchDropWindows(
path: .init(dropId: "b11f386a-0000-4000-8000-d0c5000000b1"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
windows: [.init(
startsAt: "<starts_at>",
endsAt: "<ends_at>"
)]
))
).ok.body.json
print(response){
"drop": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"code": "EXAMPLE10",
"shop_url": "https://example.com/image.jpg",
"image_url": "https://example.com/image.jpg",
"share_url": "https://example.com/image.jpg",
"name": "Example name",
"description": "Added at the front desk",
"status": "draft",
"state": "draft",
"opens_at": "<opens_at>",
"closes_at": "<closes_at>",
"collection_date": "<collection_date>",
"timezone": "Europe/London",
"checkout_ttl_seconds": 1,
"menu_visible_before_open": true,
"show_stock_count": true,
"max_per_customer": 1,
"items": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"item_name": "<item_name>",
"size_label": "<size_label>",
"colour_label": "<colour_label>",
"size_name": "<size_name>",
"colour_name": "<colour_name>",
"colour_photo_url": "https://example.com/image.jpg",
"price_pence": 1500,
"usual_price_pence": 1500,
"drop_price_pence": 1500,
"buyer_price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"per_bundle": -9007199254740991,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true,
"is_food": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1,
"booked_count": 1,
"booked_names": [
"<booked_name>"
],
"removable": true,
"in_dispute": true
}
],
"moves_not_reached": {
"not_told": -9007199254740991,
"not_told_names": [
"<not_told_name>"
]
},
"buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"cancelled_buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"still_to_come_pence": 1500,
"unpaid_orders_count": 1,
"bundle_price_pence": 1500,
"bundle_buyer_price_pence": 1500,
"bundle_total": 1,
"bundle_remaining": 1,
"is_bundle": true,
"blockers": [
"<blocker>"
],
"blocker_codes": [
{
"code": "no_items",
"names": [
"Example name"
]
}
],
"making_started_at": "<making_started_at>",
"created_at": "<created_at>",
"updated_at": "<updated_at>"
},
"moved_bookings": {
"orders": -9007199254740991,
"told": -9007199254740991,
"not_told": -9007199254740991,
"not_told_names": [
"<not_told_name>"
]
}
}Saves one ask on a size and colour of an item in this drop that is gone: one email when it can be bought again in this drop or the shop's next, then the ask has ended. PUBLIC, no account needed. Does NOT join the shop's list. Every answer but available reads the same, whatever the address. Refused (404) until it is switched on.
Where the one email goes. Required, and exactly one address, or the request is refused.
savedavailablecurl -X POST "https://www.membber.com/api/v1/drops/back-ask/EXAMPLE10" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"drop_line_id": "b01fc3f6-0000-4000-8000-d0c5000000b0",
"email": "alex@example.com"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/back-ask/{dropCode}", {
params: { path: { dropCode: "EXAMPLE10" } },
body: {
drop_line_id: "b01fc3f6-0000-4000-8000-d0c5000000b0",
email: "alex@example.com"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.askWhenBack(
path: .init(dropCode: "EXAMPLE10"),
body: .json(.init(
dropLineId: "b01fc3f6-0000-4000-8000-d0c5000000b0",
email: "alex@example.com"
))
).ok.body.json
print(response){
"ok": true,
"outcome": "saved"
}Membber Drops only. Everyone who bought from one of her drops, joined her alert list or asked a question, with what they paid, where they came from and whether they are on her alert list. Deleted people are never listed or counted. Names and contacts are the ones this shop was given. Requires Membber Drops.
The shop (also authorises the merchant).
Only people who first came through this door.
drop_purchasedrop_alertsdrop_questionComma-separated AND filters: has_ordered, on_alert_list. Loyalty filters answer 400 NOT_IN_DROPS_SCOPE.
Narrows has_ordered to people who ordered from this drop.
recent (default), paid (most first) or name. Id breaks ties; nameless people sort last.
recentpaidnameName, email or mobile this shop was given. Ignored under 2 characters.
Page size (default 30, max 100). 0 returns counts and the total only.
Page offset.
The latest name typed at her checkout. Null when she was never given one. Never the name on their shared account, which another shop may have been given.
Store-received email. The title when there is no name.
Store-received mobile, as written, and only when this shop has no name and no email for them: it is then the row's title. Null otherwise.
How they first came to this shop, whichever door it was. Null only when no first touch was recorded.
The door that first brought them to this shop, as recorded. drop_purchase, drop_alerts or drop_question when a drop did; otherwise the door from before her drops (own_app, app_clip, shared_app, merchant, import, web), which clients word with the Business app's came-from words. Clients word it themselves.
drop_purchasedrop_alertsdrop_questionown_appapp_clipshared_appmerchantimportwebThe drop that won them, as named at the time. Null when not recorded, and always null for a door that is not a drop.
When they first met this shop (ISO 8601).
Drop orders bought (any purchased status, refunds included).
Drop orders placed and still to be paid at collection.
What they paid on her drops after refunds, including the Membber buyer fee.
What is still to pay on placed-but-unpaid drop orders.
Latest of their last drop order, first visit and alert-list signup.
On her alert list now: joined, and not stopped. A per-drop "Remind me" is never on.
When they said yes. Null when they were never on the list.
How the list reaches them. Empty unless `on`.
smsemailWhen they stopped hearing from her. Null while on the list.
Wholly blocked by this shop.
Channels this shop has muted for them.
emailsmsdrops_chatapp_messageCounted after q and drop_id and before from and facets, so each pill shows what pressing it gives.
The shop's currency, lower-case ISO 4217.
curl -G "https://www.membber.com/api/v1/drops/buyers" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/buyers", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.listDropBuyers(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"email": "alex@example.com",
"phone": "+44 7700 900123",
"came_from": {
"via": "drop_purchase",
"drop_name": "<drop_name>",
"at": "<at>"
},
"order_count": 1,
"awaiting_payment_count": 1,
"paid_pence": 1500,
"awaiting_pence": 1500,
"last_activity_at": "<last_activity_at>",
"alert": {
"on": true,
"since": "<since>",
"channels": [
"sms"
],
"stopped_at": "<stopped_at>"
},
"blocked": true,
"blocked_channels": [
"email"
]
}
],
"counts": {
"everyone": -9007199254740991,
"bought": -9007199254740991,
"alerts": -9007199254740991,
"asked": -9007199254740991,
"has_ordered": -9007199254740991,
"on_alert_list": -9007199254740991
},
"currency": "GBP",
"pagination": {
"limit": 25,
"offset": 0,
"has_more": true,
"total_count": 1
}
}Membber Drops only. Their name and contacts as this shop received them, how they first came, their place on her alert list (with the exact words they agreed to), and their drop orders. 404 alike for a deleted person, someone who is not her customer, and someone her drops did not bring. Requires Membber Drops.
The shop (also authorises the merchant).
From `orders_next_cursor`, for the next 25 orders.
Only a name this shop was given at checkout. Null when none; never the name on their shared account.
Only an email this shop was given.
Only a mobile this shop was given.
How they first came to this shop, whichever door it was. Null only when no first touch was recorded.
The door that first brought them to this shop, as recorded. drop_purchase, drop_alerts or drop_question when a drop did; otherwise the door from before her drops (own_app, app_clip, shared_app, merchant, import, web), which clients word with the Business app's came-from words. Clients word it themselves.
drop_purchasedrop_alertsdrop_questionown_appapp_clipshared_appmerchantimportwebThe drop that won them, as named at the time. Null when not recorded, and always null for a door that is not a drop.
When they first met this shop (ISO 8601).
On her alert list now: joined, and not stopped. A per-drop "Remind me" is never on.
When they said yes. Null when they were never on the list.
How the list reaches them. Empty unless `on`.
smsemailWhen they stopped hearing from her. Null while on the list.
Where they said yes, e.g. shop_page.
The exact words they agreed to.
The latest change to their alerts: who, how and when. Null when they were never on her list.
onoffWhen (ISO 8601).
themyoushopnot_recordedtext_link / email_link: the stop link in a text or an email (or their mail app's unsubscribe button); link: a stop link that did not say where it was sent; shop_page: they joined on her shop page; drop_reminder: a Remind me; switch: the seller's switch; contact_corrected: she corrected that contact. Null when not recorded.
text_linkemail_linklinkshop_pagedrop_reminderswitchcontact_correctedThe staff member's name when `who` is shop and their profile has one.
She ticked "They asked me to" when turning them back on.
What the switch does now. turn_off: on, one tap turns them off. turn_on: off by her own switch, one tap turns them back on. turn_on_if_asked: they stopped them (or nobody recorded who), so only with "They asked me to". none: never on her list, or only on a contact she corrected (they join again on her shop page).
turn_offturn_onturn_on_if_askednoneWhere her alerts reach them now: the mobiles, then the addresses, on her list for them. Empty unless on.
Read-only here; changed in the Business app.
emailsmsdrops_chatapp_messageWhether this signed-in person may start a chat with them now. Decided on the server.
The latest data request this shop forwarded to Membber for them.
copyremoveDrop orders bought (any purchased status, disputed and refunded included).
What they paid on her drops after refunds, including the Membber buyer fee.
The orders whose money is in paid_pence: settled, and not wholly refunded. Disputed orders are not among them. Use this, not order_count, for "paid on N orders".
Their drop orders, newest first, 25 at a time.
True when the buyer's bank took the money back: a dispute the shop lost, in full or in part. The order then says "Dispute lost", never "Refunded" (its status still reads refunded or partially_refunded). False for a refund the shop made. Absent on an answer from before 12 Oct 2026.
curl -G "https://www.membber.com/api/v1/drops/buyers/a1c10999-0000-4000-8000-d0c5000000a1" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/buyers/{customerId}", {
params: { path: { customerId: "a1c10999-0000-4000-8000-d0c5000000a1" }, query: { store_id: "6659c139-0000-4000-8000-d0c500000066" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getDropBuyer(
path: .init(customerId: "a1c10999-0000-4000-8000-d0c5000000a1"),
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"id": "00000d1b-0000-4000-8000-d0c500000000",
"identity": {
"name": "Example name",
"email": "alex@example.com",
"phone": "+44 7700 900123"
},
"came_from": {
"via": "drop_purchase",
"drop_name": "<drop_name>",
"at": "<at>"
},
"alert": {
"on": true,
"since": "<since>",
"channels": [
"sms"
],
"stopped_at": "<stopped_at>",
"source": "<source>",
"words": "<words>",
"last_change": {
"action": "on",
"at": "<at>",
"who": "them",
"how": "text_link",
"name": "Example name",
"they_asked": true
},
"switch": "turn_off",
"to": [
"<to>"
]
},
"block": {
"blocked": true,
"channels": [
"email"
]
},
"can_message": true,
"data_request": {
"forwarded_at": "<forwarded_at>",
"kind": "copy"
},
"totals": {
"order_count": 1,
"awaiting_payment_count": 1,
"paid_pence": 1500,
"paid_order_count": 1,
"awaiting_pence": 1500
},
"orders": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"batch_drop_id": "63a4cc66-0000-4000-8000-d0c500000063",
"drop_name": "<drop_name>",
"status": "<status>",
"total_pence": 1500,
"refunded_pence": 1500,
"created_at": "<created_at>",
"collection_at": "<collection_at>",
"paid_at": "<paid_at>",
"payment_method_kind": "<payment_method_kind>",
"dispute_lost": true
}
],
"orders_next_cursor": "<orders_next_cursor>",
"currency": "GBP"
}Membber Drops. Off is immediate. Back on is refused (409 THEY_ASKED_NEEDED) after they stopped alerts themselves unless they_asked is true, and they are then sent one message saying so, with a way to stop. Someone who never joined her alert list is never switched on (409 NOT_ON_ALERT_LIST), and neither is a contact she corrected (409 CONTACT_CORRECTED). Every change is recorded with who made it and how. The same 404 as the customer page for someone who is not hers. Idempotent: setting the state they are already in changes nothing.
The shop. May travel in the body or as ?store_id=; the authenticated store wins.
true: back on. false: off.
She confirms they asked her to turn their alerts back on. Needed when they stopped them themselves (or nobody recorded who did); recorded with her name and the time.
On her alert list now: joined, and not stopped. A per-drop "Remind me" is never on.
When they said yes. Null when they were never on the list.
How the list reaches them. Empty unless `on`.
smsemailWhen they stopped hearing from her. Null while on the list.
Where they said yes, e.g. shop_page.
The exact words they agreed to.
The latest change to their alerts: who, how and when. Null when they were never on her list.
onoffWhen (ISO 8601).
themyoushopnot_recordedtext_link / email_link: the stop link in a text or an email (or their mail app's unsubscribe button); link: a stop link that did not say where it was sent; shop_page: they joined on her shop page; drop_reminder: a Remind me; switch: the seller's switch; contact_corrected: she corrected that contact. Null when not recorded.
text_linkemail_linklinkshop_pagedrop_reminderswitchcontact_correctedThe staff member's name when `who` is shop and their profile has one.
She ticked "They asked me to" when turning them back on.
What the switch does now. turn_off: on, one tap turns them off. turn_on: off by her own switch, one tap turns them back on. turn_on_if_asked: they stopped them (or nobody recorded who), so only with "They asked me to". none: never on her list, or only on a contact she corrected (they join again on her shop page).
turn_offturn_onturn_on_if_askednoneWhere her alerts reach them now: the mobiles, then the addresses, on her list for them. Empty unless on.
How many of their alert-list sign-ups changed. 0 when they were already that way.
Back on after they had stopped them: the ONE message that told them so, on the contact that is back on, with a way to stop. none when nothing was sent (switched off, back on after her own switch, or no way to reach them).
textemailnonecurl -X PATCH "https://www.membber.com/api/v1/drops/buyers/a1c10999-0000-4000-8000-d0c5000000a1/alerts" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"on": true
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PATCH("/api/v1/drops/buyers/{customerId}/alerts", {
params: { path: { customerId: "a1c10999-0000-4000-8000-d0c5000000a1" } },
body: {
on: true
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.setDropBuyerAlerts(
path: .init(customerId: "a1c10999-0000-4000-8000-d0c5000000a1"),
body: .json(.init(
on: true
))
).ok.body.json
print(response){
"alert": {
"on": true,
"since": "<since>",
"channels": [
"sms"
],
"stopped_at": "<stopped_at>",
"source": "<source>",
"words": "<words>",
"last_change": {
"action": "on",
"at": "<at>",
"who": "them",
"how": "text_link",
"name": "Example name",
"they_asked": true
},
"switch": "turn_off",
"to": [
"<to>"
]
},
"changed": -9007199254740991,
"told": "text"
}When a buyer asks the shop for a copy of their details, or to be removed, the shop forwards it here. Membber checks it is really them, replies within the legal time and tells the shop. Nothing is deleted or exported automatically. A repeat within 30 days is answered with the first one. Works even if Membber Drops has lapsed.
The shop (also authorises the merchant).
copy: they asked for a copy of their details. remove: they asked to be removed.
copyremoveThe day they asked her (YYYY-MM-DD).
Anything Membber should know. Sent on, never stored.
copyremoveTrue when this shop already forwarded the same request in the last 30 days; nothing new was sent.
A short reference Membber support can find the request by.
curl -X POST "https://www.membber.com/api/v1/drops/buyers/a1c10999-0000-4000-8000-d0c5000000a1/data-request" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"kind": "copy"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/buyers/{customerId}/data-request", {
params: { path: { customerId: "a1c10999-0000-4000-8000-d0c5000000a1" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
kind: "copy"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.forwardDropBuyerDataRequest(
path: .init(customerId: "a1c10999-0000-4000-8000-d0c5000000a1"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
kind: .copy
))
).ok.body.json
print(response){
"forwarded_at": "<forwarded_at>",
"kind": "copy",
"already": true,
"reference": "<reference>"
}Authorised by the PaymentIntent's client_secret on a card order, or by the order page token on an order placed to pay later, each held only by the browser that placed the order. Never minted from a remembered email: that is typed, never verified.
curl -X POST "https://www.membber.com/api/v1/drops/chat/claim" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"client_secret": "<client_secret>",
"order_number": "<order_number>",
"order_token": "<order_token>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/chat/claim", {
body: {
client_secret: "<client_secret>",
order_number: "<order_number>",
order_token: "<order_token>"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.claimDropChat(
body: .json(.init(
clientSecret: "<client_secret>",
orderNumber: "<order_number>",
orderToken: "<order_token>"
))
).ok.body.json
print(response){
"ok": true,
"chat_token": "<chat_token>",
"handle": "<handle>"
}Merchant, her own store (store_id). Her whole preview menu as suggestions she confirms: names, her own words, any price her preview showed (her caption or her post or reel, marked from her post; her own website; or a price we set, marked "Suggested price"), its heading and the photo she kept (in her storage), only until her first drop is published. Never allergens, a day, a time, an address or a way to pay: the preview never held them.
captionwebsiteinstagram_mediaset_by_usfixedfromcurl -G "https://www.membber.com/api/v1/drops/claimed-preview" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/claimed-preview");
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.dropsClaimedPreview().ok.body.json
print(response){
"prefill": {
"kind": "<kind>",
"items": [
{
"name": "Example name",
"price_pence": 1500,
"price_from_post": true,
"price_source": "caption",
"source_url": "https://example.com/image.jpg",
"price_kind": "fixed",
"price_suffix": "<price_suffix>",
"description": "Added at the front desk",
"category": "<category>",
"enquire": true,
"photo_url": "https://example.com/image.jpg"
}
],
"photos": [
"<photo>"
]
}
}The files of one download, in the seller's order: those ready and those still uploading. Files she removed that can still be brought back are listed apart, in `removed`. Never a link to a file. 404 until downloads are switched on.
The download whose files to list.
filevideopendingreadyfilevideocurl -G "https://www.membber.com/api/v1/drops/digital/assets" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "menu_item_id=ca65a6e7-0000-4000-8000-d0c5000000ca"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/digital/assets", {
params: { query: { menu_item_id: "ca65a6e7-0000-4000-8000-d0c5000000ca" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.listDropsDigitalAssets(
query: .init(menuItemId: "ca65a6e7-0000-4000-8000-d0c5000000ca")
).ok.body.json
print(response){
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"is_complete": true,
"max_files": -9007199254740991,
"max_bytes": -9007199254740991,
"max_video_bytes": -9007199254740991,
"max_videos": -9007199254740991,
"assets": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"kind": "file",
"original_filename": "<original_filename>",
"content_type": "<content_type>",
"byte_size": -9007199254740991,
"duration_seconds": -9007199254740991,
"status": "pending",
"sort_order": -9007199254740991,
"created_at": "<created_at>"
}
],
"removed": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"kind": "file",
"original_filename": "<original_filename>",
"content_type": "<content_type>",
"byte_size": -9007199254740991,
"duration_seconds": -9007199254740991,
"removed_at": "<removed_at>",
"kept_until": "<kept_until>"
}
]
}The server chooses where the file goes and returns a one-use signed upload target; the client sends the bytes straight to storage and then calls confirm. The file is `pending` and counts for nothing until confirmed. A PDF up to 50 MB or an MP4 video up to 50 MB; at most 10 files on one download, of which at most 3 videos (422 TOO_MANY_FILES, 422 TOO_MANY_VIDEOS). A video is sent in parts to `upload.resumable`. 404 until downloads are switched on.
The download this file belongs to.
The file's own name, shown to the seller and later to the buyer. Never used as its storage name.
A PDF (`application/pdf`) or an MP4 video (`video/mp4`).
application/pdfvideo/mp4The size the client is about to send: up to 50 MB for a PDF, up to 50 MB for a video. Checked again, from storage, when the upload is confirmed.
filevideopendingreadycurl -X POST "https://www.membber.com/api/v1/drops/digital/assets" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"filename": "<filename>",
"content_type": "application/pdf",
"byte_size": 1
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/digital/assets", {
body: {
menu_item_id: "ca65a6e7-0000-4000-8000-d0c5000000ca",
filename: "<filename>",
content_type: "application/pdf",
byte_size: 1
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.startDropsDigitalAssetUpload(
body: .json(.init(
menuItemId: "ca65a6e7-0000-4000-8000-d0c5000000ca",
filename: "<filename>",
contentType: .application/pdf,
byteSize: 1
))
).ok.body.json
print(response){
"asset": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"kind": "file",
"original_filename": "<original_filename>",
"content_type": "<content_type>",
"byte_size": -9007199254740991,
"duration_seconds": -9007199254740991,
"status": "pending",
"sort_order": -9007199254740991,
"created_at": "<created_at>"
},
"upload": {
"path": "<path>",
"token": "<token>",
"signed_url": "https://example.com/image.jpg",
"resumable": {
"endpoint": "<endpoint>",
"bucket": "<bucket>",
"chunk_bytes": -9007199254740991
}
},
"max_bytes": -9007199254740991
}The file leaves the download and can be brought back with restore for 30 days (`kept_until`); only after that is its stored file deleted for good. An upload that was never confirmed is deleted at once. Removing the last ready file makes the download incomplete. Removing a file twice is not an error. 404 until downloads are switched on.
curl -X DELETE "https://www.membber.com/api/v1/drops/digital/assets/d5fdf82b-0000-4000-8000-d0c5000000d5" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.DELETE("/api/v1/drops/digital/assets/{assetId}", {
params: { path: { assetId: "d5fdf82b-0000-4000-8000-d0c5000000d5" } },
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.removeDropsDigitalAsset(
path: .init(assetId: "d5fdf82b-0000-4000-8000-d0c5000000d5")
).ok.body.json
print(response){
"removed": true,
"asset_id": "e9c160ca-0000-4000-8000-d0c5000000e9",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"is_complete": true,
"kept_until": "<kept_until>"
}The server reads the stored object itself: it must exist, be within the size limit of its type, and be that type (a PDF, or an MP4 video) by its declared type AND by its first bytes. A file that fails is deleted at once and the answer is 422 UPLOAD_REFUSED. A file that passes becomes `ready`. Confirming a ready file again returns it unchanged. 404 until downloads are switched on.
A video only: its length in whole seconds, read on the seller's device from the video's own metadata. Stored and shown, never enforced. Ignored for a PDF.
filevideopendingreadycurl -X POST "https://www.membber.com/api/v1/drops/digital/assets/d5fdf82b-0000-4000-8000-d0c5000000d5/confirm" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"asset_id": "e9c160ca-0000-4000-8000-d0c5000000e9",
"duration_seconds": 1
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/digital/assets/{assetId}/confirm", {
params: { path: { assetId: "d5fdf82b-0000-4000-8000-d0c5000000d5" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
asset_id: "e9c160ca-0000-4000-8000-d0c5000000e9",
duration_seconds: 1
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.confirmDropsDigitalAsset(
path: .init(assetId: "d5fdf82b-0000-4000-8000-d0c5000000d5"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
assetId: "e9c160ca-0000-4000-8000-d0c5000000e9",
durationSeconds: 1
))
).ok.body.json
print(response){
"asset": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"kind": "file",
"original_filename": "<original_filename>",
"content_type": "<content_type>",
"byte_size": -9007199254740991,
"duration_seconds": -9007199254740991,
"status": "pending",
"sort_order": -9007199254740991,
"created_at": "<created_at>"
},
"is_complete": true
}Undoes a remove: the file is `ready` on its download again, exactly as it was. Works while the stored file is still kept (at least 30 days after the removal) and the download holds fewer than 10 files. 410 FILE_GONE once the stored file has been deleted, 422 TOO_MANY_FILES when the download is full, 422 TOO_MANY_VIDEOS when it is a video and the download already holds 3. Restoring a file that is not removed returns it unchanged. 404 until downloads are switched on.
filevideopendingreadycurl -X POST "https://www.membber.com/api/v1/drops/digital/assets/d5fdf82b-0000-4000-8000-d0c5000000d5/restore" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"asset_id": "e9c160ca-0000-4000-8000-d0c5000000e9"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/digital/assets/{assetId}/restore", {
params: { path: { assetId: "d5fdf82b-0000-4000-8000-d0c5000000d5" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
asset_id: "e9c160ca-0000-4000-8000-d0c5000000e9"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.restoreDropsDigitalAsset(
path: .init(assetId: "d5fdf82b-0000-4000-8000-d0c5000000d5"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
assetId: "e9c160ca-0000-4000-8000-d0c5000000e9"
))
).ok.body.json
print(response){
"asset": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"kind": "file",
"original_filename": "<original_filename>",
"content_type": "<content_type>",
"byte_size": -9007199254740991,
"duration_seconds": -9007199254740991,
"status": "pending",
"sort_order": -9007199254740991,
"created_at": "<created_at>"
},
"is_complete": true
}PUBLIC: the person has no account. For a download the shop made free: emails the address the download's private link, and keeps the sentence it was shown with the claim. Nobody is joined to the shop's list by this call: the address joins when the file is first taken through that link. Not an order. The answer is `{ ok: true }` for every address (new or not, sent now or held back, and for an address the shop has blocked, which is sent nothing) and never carries the link: the link is only in the email. A second claim by the same inbox (its case and any plus-tag ignored) sends the email again, within limits, and leaves the words already stored as they are. One inbox is sent at most 4 of these emails an hour and 8 a day by one shop, and 6 an hour and 12 a day by all shops. Refused when the download has a price, is not complete or has no ready file, when the shop is not live on Drops, and when the sentence is not the server's own. 404 until downloads are switched on.
The free download to send.
Where the download is sent. Required: exactly one address.
The sentence the person was shown, word for word: "Free when you join {shop}'s list. Unsubscribe any time." with the shop's name in it. A sentence that is not the one the server would store is refused (409 CONSENT_CHANGED).
curl -X POST "https://www.membber.com/api/v1/drops/digital/claim/<handle>" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"email": "alex@example.com",
"consent_text": "<consent_text>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/digital/claim/{handle}", {
params: { path: { handle: "<handle>" } },
body: {
menu_item_id: "ca65a6e7-0000-4000-8000-d0c5000000ca",
email: "alex@example.com",
consent_text: "<consent_text>"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.claimDropsDigitalFree(
path: .init(handle: "<handle>"),
body: .json(.init(
menuItemId: "ca65a6e7-0000-4000-8000-d0c5000000ca",
email: "alex@example.com",
consentText: "<consent_text>"
))
).ok.body.json
print(response){
"ok": true
}One row for each of her downloads that anybody holds: how many were paid for and not closed by a refund or a lost dispute, how many were taken free for an email, and how many times they were downloaded. Her own test copies are counted in none of them. A download she has taken off her menu still answers. 404 until downloads are switched on.
curl -G "https://www.membber.com/api/v1/drops/digital/counts" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/digital/counts");
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.listDropsDigitalCounts().ok.body.json
print(response){
"counts": [
{
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"sold": -9007199254740991,
"free": -9007199254740991,
"downloads": -9007199254740991
}
]
}The grants of one download, newest first, at most 50: each sale, free claim and seller test, with whether its email was sent, how often it was downloaded and whether it was closed. Never a link or a token. A download she has taken off her menu still answers, because its buyers keep it. 404 until downloads are switched on.
The download whose grants to list.
purchasefree_claimseller_testopenrevokedrefundeddisputedsellersupportcurl -G "https://www.membber.com/api/v1/drops/digital/grants" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "menu_item_id=ca65a6e7-0000-4000-8000-d0c5000000ca"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/digital/grants", {
params: { query: { menu_item_id: "ca65a6e7-0000-4000-8000-d0c5000000ca" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.listDropsDigitalGrants(
query: .init(menuItemId: "ca65a6e7-0000-4000-8000-d0c5000000ca")
).ok.body.json
print(response){
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"download_cap": -9007199254740991,
"grants": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"source": "purchase",
"email": "alex@example.com",
"order_number": "<order_number>",
"granted_at": "<granted_at>",
"status": "open",
"revoked_at": "<revoked_at>",
"revoked_reason": "refunded",
"download_count": 1,
"download_total": -9007199254740991,
"last_downloaded_at": "<last_downloaded_at>",
"capped": true,
"delivery_sent_at": "<delivery_sent_at>",
"delivery_attempts": -9007199254740991
}
]
}PUBLIC: a buyer has no account. The token from the email is the credential and is checked on every call. Counts one download and returns a signed address for ONE named file, good for 10 minutes. The file must be a ready file of this grant's own download: another download's or another shop's file is 404. A wrong, missing or expired token and a closed grant are all the same 404. 429 DOWNLOAD_LIMIT after 20 downloads: the shop can send it again. 404 until downloads are switched on. A FREE download claimed for an email joins its address to the shop's list on the first take (this call proves the address: it carries the token from the email), and that one answer carries `joined_list.leave_path`.
The access token from the link in the email (its `t` value).
The file to download: one of this grant's own download's ready files.
Only for a FREE download claimed for an email, and only on the take that joined the address to the shop's list (the first take; never again for someone already on the list or who has left since). Absent on every other take and for every download that was paid for.
The shop list's own stop page for this person, as a path on this site (`/u/{token}`). A POST to it stops the messages.
curl -X POST "https://www.membber.com/api/v1/drops/digital/grants/10b4f357-0000-4000-8000-d0c500000010/download" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"t": "<t>",
"asset_id": "e9c160ca-0000-4000-8000-d0c5000000e9"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/digital/grants/{grantId}/download", {
params: { path: { grantId: "10b4f357-0000-4000-8000-d0c500000010" } },
body: {
t: "<t>",
asset_id: "e9c160ca-0000-4000-8000-d0c5000000e9"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.takeDropsDigitalDownload(
path: .init(grantId: "10b4f357-0000-4000-8000-d0c500000010"),
body: .json(.init(
t: "<t>",
assetId: "e9c160ca-0000-4000-8000-d0c5000000e9"
))
).ok.body.json
print(response){
"url": "https://example.com/image.jpg",
"expires_in_seconds": -9007199254740991,
"filename": "<filename>",
"downloads_left": -9007199254740991,
"joined_list": {
"leave_path": "<leave_path>"
}
}PUBLIC: a buyer has no account. The token from the email is the credential and is checked on every call. Returns a signed address the page plays ONE named video from, good for an hour. It counts nothing: playing does not use up downloads. A POST, never made when the page is opened, so a mail scanner that follows the link is handed no address. The video must be a ready video of this grant's own download: a PDF, another download's or another shop's file is 404. A wrong, missing or expired token and a closed grant (refunded, disputed) are all the same 404. 429 RATE_LIMITED after 20 addresses for one grant in ten minutes. 404 until downloads are switched on.
The access token from the link in the email (its `t` value).
The video to play: one of this grant's own download's ready videos.
Only for a FREE download claimed for an email, and only on the call that joined the address to the shop's list (watching it is taking it). Absent on every other call and for every download that was paid for.
The shop list's own stop page for this person, as a path on this site (`/u/{token}`).
curl -X POST "https://www.membber.com/api/v1/drops/digital/grants/10b4f357-0000-4000-8000-d0c500000010/play" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"t": "<t>",
"asset_id": "e9c160ca-0000-4000-8000-d0c5000000e9"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/digital/grants/{grantId}/play", {
params: { path: { grantId: "10b4f357-0000-4000-8000-d0c500000010" } },
body: {
t: "<t>",
asset_id: "e9c160ca-0000-4000-8000-d0c5000000e9"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.playDropsDigitalVideo(
path: .init(grantId: "10b4f357-0000-4000-8000-d0c500000010"),
body: .json(.init(
t: "<t>",
assetId: "e9c160ca-0000-4000-8000-d0c5000000e9"
))
).ok.body.json
print(response){
"url": "https://example.com/image.jpg",
"expires_in_seconds": -9007199254740991,
"joined_list": {
"leave_path": "<leave_path>"
}
}Sends the same email again, to the same address, with a fresh link good for a year, and sets the download count back to 0 so a buyer who reached the limit can download again. 409 GRANT_REVOKED for a closed grant, 409 NO_ADDRESS when the buyer's details were deleted, 429 SEND_LIMIT after ten sends, 502 EMAIL_FAILED when the email could not be sent. 404 until downloads are switched on.
purchasefree_claimseller_testopenrevokedrefundeddisputedsellersupportcurl -X POST "https://www.membber.com/api/v1/drops/digital/grants/10b4f357-0000-4000-8000-d0c500000010/resend" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"grant_id": "05e9cb1e-0000-4000-8000-d0c500000005"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/digital/grants/{grantId}/resend", {
params: { path: { grantId: "10b4f357-0000-4000-8000-d0c500000010" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
grant_id: "05e9cb1e-0000-4000-8000-d0c500000005"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.resendDropsDigitalGrant(
path: .init(grantId: "10b4f357-0000-4000-8000-d0c500000010"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
grantId: "05e9cb1e-0000-4000-8000-d0c500000005"
))
).ok.body.json
print(response){
"grant": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"source": "purchase",
"email": "alex@example.com",
"order_number": "<order_number>",
"granted_at": "<granted_at>",
"status": "open",
"revoked_at": "<revoked_at>",
"revoked_reason": "refunded",
"download_count": 1,
"download_total": -9007199254740991,
"last_downloaded_at": "<last_downloaded_at>",
"capped": true,
"delivery_sent_at": "<delivery_sent_at>",
"delivery_attempts": -9007199254740991
}
}Emails the signed-in seller the same email a buyer gets, with a working link to the download's private page, at her own sign-in address and no other. The first call makes her test grant; every later call sends it again and sets its download count back to 0. The download must hold at least one ready file (422 NO_FILES). 404 until downloads are switched on.
The download to send a test copy of.
purchasefree_claimseller_testopenrevokedrefundeddisputedsellersupportcurl -X POST "https://www.membber.com/api/v1/drops/digital/grants/test-copy" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/digital/grants/test-copy", {
body: {
menu_item_id: "ca65a6e7-0000-4000-8000-d0c5000000ca"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.sendDropsDigitalTestCopy(
body: .json(.init(
menuItemId: "ca65a6e7-0000-4000-8000-d0c5000000ca"
))
).ok.body.json
print(response){
"grant": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"source": "purchase",
"email": "alex@example.com",
"order_number": "<order_number>",
"granted_at": "<granted_at>",
"status": "open",
"revoked_at": "<revoked_at>",
"revoked_reason": "refunded",
"download_count": 1,
"download_total": -9007199254740991,
"last_downloaded_at": "<last_downloaded_at>",
"capped": true,
"delivery_sent_at": "<delivery_sent_at>",
"delivery_attempts": -9007199254740991
},
"sent_to": "<sent_to>"
}Every paid order for a download, newest first, at most 50, with the price, the 5% the buyer paid on top, what went back and what she kept, each a figure the order recorded; whether it could not be sent; and its grant, for Send again. The totals count every download order, listed or not. A download order belongs to no drop, so this is where it shows. Never a link or a token. 404 until downloads are switched on.
paidpart_refundedrefundeddisputeddispute_lostopenrevokedcurl -G "https://www.membber.com/api/v1/drops/digital/orders" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/digital/orders");
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.listDropsDigitalOrders().ok.body.json
print(response){
"orders": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"paid_at": "<paid_at>",
"status": "<status>",
"state": "paid",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"item_name": "<item_name>",
"email": "alex@example.com",
"goods_pence": 1500,
"buyer_fee_pence": 1500,
"total_pence": 1500,
"refunded_pence": 1500,
"dispute_lost_pence": 1500,
"taken_pence": 1500,
"stripe_mode": "<stripe_mode>",
"not_sent": true,
"grant_id": "05e9cb1e-0000-4000-8000-d0c500000005",
"grant_status": "open",
"downloads": -9007199254740991
}
],
"orders_count": 1,
"taken_pence": 1500,
"given_back_pence": 1500,
"not_sent_count": 1
}For a download order that is paid for and flagged as not sent: tries again to make the buyer's grant and send its one email and the receipt, exactly as the payment would have. Safe to call again: a grant that exists is found, and an email that went is not sent twice. Answers the order as it now stands. 409 ORDER_NOT_SENDABLE for an order that is refunded, disputed or not paid, 502 DOWNLOAD_NOT_SENT when it still could not be sent (refund it instead). An order that was already sent is answered as it is: use the grant's own resend to email it again. 404 until downloads are switched on.
paidpart_refundedrefundeddisputeddispute_lostopenrevokedcurl -X POST "https://www.membber.com/api/v1/drops/digital/orders/b80cf509-0000-4000-8000-d0c5000000b8/send" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"order_id": "4991ffac-0000-4000-8000-d0c500000049"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/digital/orders/{orderId}/send", {
params: { path: { orderId: "b80cf509-0000-4000-8000-d0c5000000b8" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
order_id: "4991ffac-0000-4000-8000-d0c500000049"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.sendDropsDigitalOrder(
path: .init(orderId: "b80cf509-0000-4000-8000-d0c5000000b8"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
orderId: "4991ffac-0000-4000-8000-d0c500000049"
))
).ok.body.json
print(response){
"order": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"paid_at": "<paid_at>",
"status": "<status>",
"state": "paid",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"item_name": "<item_name>",
"email": "alex@example.com",
"goods_pence": 1500,
"buyer_fee_pence": 1500,
"total_pence": 1500,
"refunded_pence": 1500,
"dispute_lost_pence": 1500,
"taken_pence": 1500,
"stripe_mode": "<stripe_mode>",
"not_sent": true,
"grant_id": "05e9cb1e-0000-4000-8000-d0c500000005",
"grant_status": "open",
"downloads": -9007199254740991
}
}PUBLIC: a buyer has no account. Creates the order and its PaymentIntent for ONE download and returns the client secret, so the card or wallet is taken on the shop's own page. The same order and payment path as a drop: paying is what sends the file, by email, at once. The server decides from the item whether a line is a download; a basket that mixes a download with anything else is refused. The price is the shop's price with the 5% on top. Refused, with nothing charged, without `immediate_supply_consent`, without an email, with a cash or bank transfer method, a promo code or a gift card, when the download has no ready file, or when the shop is not live on Drops with its own direct-charge Stripe account. 404 until downloads are switched on.
One line, quantity 1, in this release. Anything else is refused with a sentence.
The shop's price for the download the buyer was shown, before the 5%. Any difference from the current price is refused with 409 PRICE_CHANGED and the current price.
Where the download is sent. Required.
True when the buyer ticked "Send it now. I know I can't cancel once it's sent.". Without it the payment is refused (400 CONSENT_REQUIRED) and nothing is charged.
Leave out. A download is paid for by card or wallet: anything but `pay_now` is refused.
Leave out. A code cannot be used on a download in this release: one that is sent is refused.
Leave out. A gift card cannot pay for a download in this release: one that is sent is refused.
curl -X POST "https://www.membber.com/api/v1/drops/digital/payment" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_code": "EXAMPLE10",
"idempotency_key": "1f0e2d3c-4b5a-4678-9abc-def012345678",
"items": [
{
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"quantity": 1
}
],
"customer": {
"email": "alex@example.com"
},
"immediate_supply_consent": true
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/digital/payment", {
body: {
store_code: "EXAMPLE10",
idempotency_key: "1f0e2d3c-4b5a-4678-9abc-def012345678",
items: [
{
menu_item_id: "ca65a6e7-0000-4000-8000-d0c5000000ca",
quantity: 1
}
],
customer: {
email: "alex@example.com"
},
immediate_supply_consent: true
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.startDropsDigitalPayment(
body: .json(.init(
storeCode: "EXAMPLE10",
idempotencyKey: "1f0e2d3c-4b5a-4678-9abc-def012345678",
items: [.init(
menuItemId: "ca65a6e7-0000-4000-8000-d0c5000000ca",
quantity: 1
)],
customer: .init(
email: "alex@example.com"
),
immediateSupplyConsent: true
))
).ok.body.json
print(response){
"order_id": "4991ffac-0000-4000-8000-d0c500000049",
"order_number": "<order_number>",
"status": "<status>",
"requires_payment": true,
"client_secret": "<client_secret>",
"connected_account_id": "231b6f63-0000-4000-8000-d0c500000023",
"publishable_key": "<publishable_key>",
"charge_model": "<charge_model>",
"amount_pence": 1500,
"currency": "GBP",
"merchant_name": "<merchant_name>"
}Cards sold, balances outstanding (what the shop still owes in goods), spent to date, expired unspent and refunded. Newest first.
activevoidedrefundedfrozenexpiredpendingcurl -G "https://www.membber.com/api/v1/drops/gift-cards" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/gift-cards");
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.listDropsGiftCards().ok.body.json
print(response){
"available": true,
"summary": {
"cards_sold": -9007199254740991,
"sold_pence": 1500,
"outstanding_pence": 1500,
"spent_pence": 1500,
"expired_unspent_pence": 1500,
"refunded_pence": 1500
},
"cards": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"code_last4": "EXAMPLE10",
"status": "active",
"initial_pence": 1500,
"balance_pence": 1500,
"spent_pence": 1500,
"currency": "GBP",
"purchaser_name": "<purchaser_name>",
"purchaser_email": "alex@example.com",
"recipient_name": "<recipient_name>",
"recipient_email": "alex@example.com",
"message": "Added at the front desk",
"expires_at": "<expires_at>",
"bought_at": "<bought_at>",
"delivered_at": "<delivered_at>",
"closed_at": "<closed_at>",
"closed_reason": "Added at the front desk",
"refunded_pence": 1500
}
]
}The card (last four only) and its ledger, newest first, with the order each spend went on.
activevoidedrefundedfrozenexpiredpendingcurl -G "https://www.membber.com/api/v1/drops/gift-cards/ed89b19b-0000-4000-8000-d0c5000000ed" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/gift-cards/{giftCardId}", {
params: { path: { giftCardId: "ed89b19b-0000-4000-8000-d0c5000000ed" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getDropsGiftCard(
path: .init(giftCardId: "ed89b19b-0000-4000-8000-d0c5000000ed")
).ok.body.json
print(response){
"card": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"code_last4": "EXAMPLE10",
"status": "active",
"initial_pence": 1500,
"balance_pence": 1500,
"spent_pence": 1500,
"currency": "GBP",
"purchaser_name": "<purchaser_name>",
"purchaser_email": "alex@example.com",
"recipient_name": "<recipient_name>",
"recipient_email": "alex@example.com",
"message": "Added at the front desk",
"expires_at": "<expires_at>",
"bought_at": "<bought_at>",
"delivered_at": "<delivered_at>",
"closed_at": "<closed_at>",
"closed_reason": "Added at the front desk",
"refunded_pence": 1500
},
"ledger": [
{
"kind": "<kind>",
"amount_pence": 1500,
"balance_after_pence": 1500,
"order_number": "<order_number>",
"note": "Added at the front desk",
"at": "<at>"
}
]
}The unspent balance is refunded to the buyer on the shop's Stripe account and the card is closed. If Stripe refuses, the card is reopened with its balance. Needs the refund permission.
activevoidedrefundedfrozenexpiredpendingcurl -X POST "https://www.membber.com/api/v1/drops/gift-cards/ed89b19b-0000-4000-8000-d0c5000000ed/refund" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"gift_card_id": "5c203c9b-0000-4000-8000-d0c50000005c",
"reason": "Added at the front desk",
"attempt_id": "70148fcd-0000-4000-8000-d0c500000070"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/gift-cards/{giftCardId}/refund", {
params: { path: { giftCardId: "ed89b19b-0000-4000-8000-d0c5000000ed" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
gift_card_id: "5c203c9b-0000-4000-8000-d0c50000005c",
reason: "Added at the front desk",
attempt_id: "70148fcd-0000-4000-8000-d0c500000070"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.refundDropsGiftCard(
path: .init(giftCardId: "ed89b19b-0000-4000-8000-d0c5000000ed"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
giftCardId: "5c203c9b-0000-4000-8000-d0c50000005c",
reason: "Added at the front desk",
attemptId: "70148fcd-0000-4000-8000-d0c500000070"
))
).ok.body.json
print(response){
"card": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"code_last4": "EXAMPLE10",
"status": "active",
"initial_pence": 1500,
"balance_pence": 1500,
"spent_pence": 1500,
"currency": "GBP",
"purchaser_name": "<purchaser_name>",
"purchaser_email": "alex@example.com",
"recipient_name": "<recipient_name>",
"recipient_email": "alex@example.com",
"message": "Added at the front desk",
"expires_at": "<expires_at>",
"bought_at": "<bought_at>",
"delivered_at": "<delivered_at>",
"closed_at": "<closed_at>",
"closed_reason": "Added at the front desk",
"refunded_pence": 1500
},
"ledger": [
{
"kind": "<kind>",
"amount_pence": 1500,
"balance_after_pence": 1500,
"order_number": "<order_number>",
"note": "Added at the front desk",
"at": "<at>"
}
]
}To the same person, with the same code. Only for a card that can still be spent.
activevoidedrefundedfrozenexpiredpendingcurl -X POST "https://www.membber.com/api/v1/drops/gift-cards/ed89b19b-0000-4000-8000-d0c5000000ed/resend" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"gift_card_id": "5c203c9b-0000-4000-8000-d0c50000005c"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/gift-cards/{giftCardId}/resend", {
params: { path: { giftCardId: "ed89b19b-0000-4000-8000-d0c5000000ed" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
gift_card_id: "5c203c9b-0000-4000-8000-d0c50000005c"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.resendDropsGiftCard(
path: .init(giftCardId: "ed89b19b-0000-4000-8000-d0c5000000ed"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
giftCardId: "5c203c9b-0000-4000-8000-d0c50000005c"
))
).ok.body.json
print(response){
"card": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"code_last4": "EXAMPLE10",
"status": "active",
"initial_pence": 1500,
"balance_pence": 1500,
"spent_pence": 1500,
"currency": "GBP",
"purchaser_name": "<purchaser_name>",
"purchaser_email": "alex@example.com",
"recipient_name": "<recipient_name>",
"recipient_email": "alex@example.com",
"message": "Added at the front desk",
"expires_at": "<expires_at>",
"bought_at": "<bought_at>",
"delivered_at": "<delivered_at>",
"closed_at": "<closed_at>",
"closed_reason": "Added at the front desk",
"refunded_pence": 1500
},
"ledger": [
{
"kind": "<kind>",
"amount_pence": 1500,
"balance_after_pence": 1500,
"order_number": "<order_number>",
"note": "Added at the front desk",
"at": "<at>"
}
]
}The remaining balance is removed and the reason kept. Needs the refund permission.
activevoidedrefundedfrozenexpiredpendingcurl -X POST "https://www.membber.com/api/v1/drops/gift-cards/ed89b19b-0000-4000-8000-d0c5000000ed/void" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"gift_card_id": "5c203c9b-0000-4000-8000-d0c50000005c",
"reason": "Added at the front desk",
"attempt_id": "70148fcd-0000-4000-8000-d0c500000070"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/gift-cards/{giftCardId}/void", {
params: { path: { giftCardId: "ed89b19b-0000-4000-8000-d0c5000000ed" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
gift_card_id: "5c203c9b-0000-4000-8000-d0c50000005c",
reason: "Added at the front desk",
attempt_id: "70148fcd-0000-4000-8000-d0c500000070"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.voidDropsGiftCard(
path: .init(giftCardId: "ed89b19b-0000-4000-8000-d0c5000000ed"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
giftCardId: "5c203c9b-0000-4000-8000-d0c50000005c",
reason: "Added at the front desk",
attemptId: "70148fcd-0000-4000-8000-d0c500000070"
))
).ok.body.json
print(response){
"card": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"code_last4": "EXAMPLE10",
"status": "active",
"initial_pence": 1500,
"balance_pence": 1500,
"spent_pence": 1500,
"currency": "GBP",
"purchaser_name": "<purchaser_name>",
"purchaser_email": "alex@example.com",
"recipient_name": "<recipient_name>",
"recipient_email": "alex@example.com",
"message": "Added at the front desk",
"expires_at": "<expires_at>",
"bought_at": "<bought_at>",
"delivered_at": "<delivered_at>",
"closed_at": "<closed_at>",
"closed_reason": "Added at the front desk",
"refunded_pence": 1500
},
"ledger": [
{
"kind": "<kind>",
"amount_pence": 1500,
"balance_after_pence": 1500,
"order_number": "<order_number>",
"note": "Added at the front desk",
"at": "<at>"
}
]
}PUBLIC. What the shop page needs to offer a gift card: whether it is on sale (and why not), the amounts, the expiry and the terms printed on the card.
curl -G "https://www.membber.com/api/v1/drops/gift-cards/public/<handle>"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.GET("/api/v1/drops/gift-cards/public/{handle}", {
params: { path: { handle: "<handle>" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.getDropsGiftCardShop(
path: .init(handle: "<handle>")
).ok.body.json
print(response){
"on_sale": true,
"unavailable_reason": "Added at the front desk",
"accepts_gift_cards": true,
"shop_name": "<shop_name>",
"currency": "GBP",
"min_pence": 1500,
"max_pence": 1500,
"preset_pence": [
1500
],
"message_max": -9007199254740991,
"expiry_months": -9007199254740991,
"terms": "<terms>"
}PUBLIC and held down hard (it is the one place a guessed code gets an answer). A wrong code and another shop's code get the same answer.
activevoidedrefundedfrozenexpiredpendingcurl -X POST "https://www.membber.com/api/v1/drops/gift-cards/public/<handle>/balance" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"code": "EXAMPLE10"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/gift-cards/public/{handle}/balance", {
params: { path: { handle: "<handle>" } },
body: {
code: "EXAMPLE10"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.checkDropsGiftCardBalance(
path: .init(handle: "<handle>"),
body: .json(.init(
code: "EXAMPLE10"
))
).ok.body.json
print(response){
"shop_name": "<shop_name>",
"status": "active",
"balance_pence": 1500,
"currency": "GBP",
"expires_at": "<expires_at>",
"usable": true
}PUBLIC. Creates a pending card and a PaymentIntent on the shop's own Stripe account. The card becomes spendable, and is emailed, only when the payment lands. Replaying the same idempotency key returns the same PaymentIntent.
The buyer's email: where the receipt goes, and the card too unless it is for someone else. Required, and exactly one address, or the request is refused.
Where the card is emailed when it is for someone else. Optional: leave it out, or send null or an empty string, and the card goes to the buyer. Anything else must be exactly one address, or the request is refused.
curl -X POST "https://www.membber.com/api/v1/drops/gift-cards/public/<handle>/purchase" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"amount_pence": 1500,
"idempotency_key": "1f0e2d3c-4b5a-4678-9abc-def012345678",
"purchaser": {
"name": "Example name",
"email": "alex@example.com"
}
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/gift-cards/public/{handle}/purchase", {
params: { path: { handle: "<handle>" } },
body: {
amount_pence: 1500,
idempotency_key: "1f0e2d3c-4b5a-4678-9abc-def012345678",
purchaser: {
name: "Example name",
email: "alex@example.com"
}
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.startDropsGiftCardPurchase(
path: .init(handle: "<handle>"),
body: .json(.init(
amountPence: 1500,
idempotencyKey: "1f0e2d3c-4b5a-4678-9abc-def012345678",
purchaser: .init(
name: "Example name",
email: "alex@example.com"
)
))
).ok.body.json
print(response){
"gift_card_id": "5c203c9b-0000-4000-8000-d0c50000005c",
"client_secret": "<client_secret>",
"publishable_key": "<publishable_key>",
"connected_account_id": "231b6f63-0000-4000-8000-d0c500000023",
"charge_model": "<charge_model>",
"amount_pence": 1500,
"currency": "GBP",
"return_path": "<return_path>",
"return_url": "https://example.com/image.jpg"
}PUBLIC. What the shop page shows after Stripe returns the buyer. Never the code.
curl -G "https://www.membber.com/api/v1/drops/gift-cards/public/<handle>/purchase/ed89b19b-0000-4000-8000-d0c5000000ed"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.GET("/api/v1/drops/gift-cards/public/{handle}/purchase/{giftCardId}", {
params: { path: { handle: "<handle>", giftCardId: "ed89b19b-0000-4000-8000-d0c5000000ed" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.getDropsGiftCardPurchase(
path: .init(handle: "<handle>", giftCardId: "ed89b19b-0000-4000-8000-d0c5000000ed")
).ok.body.json
print(response){
"gift_card_id": "5c203c9b-0000-4000-8000-d0c50000005c",
"paid": true,
"amount_pence": 1500,
"currency": "GBP",
"sent_to": "<sent_to>",
"for_someone_else": true,
"recipient_name": "<recipient_name>",
"expires_at": "<expires_at>",
"shop_name": "<shop_name>"
}Bound to the calling login and shop. web: returns `authorize_url` and sets an httpOnly binder cookie on this response, so only this signed-in browser can finish it. app: returns a one-use `launch_url` for the sign-in sheet; the sheet comes back to membber-business://instagram-import?import=…&outcome=returned&code=… and the app finishes with POST …/{importId}/complete. The OAuth return is a server page. Refused with INSTAGRAM_NOT_AVAILABLE when the feature is off for this shop.
The shop (business auth checks the caller runs it).
webappfrom_signupfrom_setupfrom_logofrom_shopfrom_claimfrom_dm_linkfulllogophotosPreviews "Make it mine": the preview being claimed.
"Use a different Instagram account": Instagram asks her to log in again.
app only (null for web): open in ASWebAuthenticationSession with scheme membber-business. Single use, 15 minutes.
web only (null for app): Instagram's login URL. Open it in the same tab; this response set the binder cookie it needs.
curl -X POST "https://www.membber.com/api/v1/drops/instagram-import" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"surface": "web",
"entry": "from_signup",
"mode": "full",
"force_reauth": true
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/instagram-import", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
surface: "web",
entry: "from_signup",
mode: "full",
force_reauth: true
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.startInstagramImport(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
surface: .web,
entry: .fromSignup,
mode: .full,
forceReauth: true
))
).ok.body.json
print(response){
"import_id": "f3921ff5-0000-4000-8000-d0c5000000f3",
"launch_url": "https://example.com",
"authorize_url": "https://example.com",
"expires_at": "<expires_at>"
}Called when she finishes or leaves. Otherwise they expire after 24 hours and the daily purge deletes them (within 48 hours at most).
The shop (business auth checks the caller runs it).
saveddiscardedcurl -X DELETE "https://www.membber.com/api/v1/drops/instagram-import/e6d327a0-0000-4000-8000-d0c5000000e6?store_id=6659c139-0000-4000-8000-d0c500000066&importId=e6d327a0-0000-4000-8000-d0c5000000e6" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.DELETE("/api/v1/drops/instagram-import/{importId}", {
params: { path: { importId: "e6d327a0-0000-4000-8000-d0c5000000e6" }, query: { store_id: "6659c139-0000-4000-8000-d0c500000066", importId: "e6d327a0-0000-4000-8000-d0c5000000e6" } },
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.discardInstagramImport(
path: .init(importId: "e6d327a0-0000-4000-8000-d0c5000000e6"),
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066", importId: "e6d327a0-0000-4000-8000-d0c5000000e6")
).ok.body.json
print(response){
"import_id": "f3921ff5-0000-4000-8000-d0c5000000f3",
"status": "saved"
}Bound to the same Membber login AND the same shop that started it. Usable for 24 hours; the daily purge then deletes it (within 48 hours at most).
The shop (business auth checks the caller runs it).
fetched | saving | saved | failed | expired | discarded | started | launched | exchanging
personal_accountcancellednot_open_yetnothing_to_importexpiredinstagram_busyfailedwebappfrom_signupfrom_setupfrom_logofrom_shopfrom_claimfrom_dm_linkfulllogophotosThe piece's Instagram media id.
The album's id, for a piece of an album post.
imagevideohttps (Instagram CDN, expiring) or a data: URL (the local fake). Null when withheld.
unavailable: Instagram withheld it (copyrighted or flagged), shown greyed. cover_only: a film that comes in as its cover.
okcover_onlyunavailableFor the tile's screen-reader label only. Never shown, never kept.
Pre-ticked: the best 6, at most one per post, never more than the shop has room for.
curl -G "https://www.membber.com/api/v1/drops/instagram-import/e6d327a0-0000-4000-8000-d0c5000000e6" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066" \
--data-urlencode "importId=e6d327a0-0000-4000-8000-d0c5000000e6"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/instagram-import/{importId}", {
params: { path: { importId: "e6d327a0-0000-4000-8000-d0c5000000e6" }, query: { store_id: "6659c139-0000-4000-8000-d0c500000066", importId: "e6d327a0-0000-4000-8000-d0c5000000e6" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getInstagramImport(
path: .init(importId: "e6d327a0-0000-4000-8000-d0c5000000e6"),
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066", importId: "e6d327a0-0000-4000-8000-d0c5000000e6")
).ok.body.json
print(response){
"import": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"status": "<status>",
"failure_reason": "personal_account",
"surface": "web",
"entry": "from_signup",
"mode": "full",
"expires_at": "<expires_at>",
"account": {
"username": "<username>",
"name": "Example name",
"picture_url": "https://example.com/image.jpg"
},
"tiles": [
{
"key": "<key>",
"parent_key": "<parent_key>",
"kind": "image",
"preview_url": "https://example.com/image.jpg",
"availability": "ok",
"caption_excerpt": "<caption_excerpt>",
"taken_at": "<taken_at>",
"already_in_shop": true,
"suggested": true
}
],
"logo": {
"offered": true,
"suggested": true
},
"name": {
"initial": "<initial>",
"offer": "<offer>"
},
"bio": {
"initial": "<initial>",
"offer": "<offer>"
},
"shop": {
"name": "Example name",
"description": "Added at the front desk",
"logo_url": "https://example.com/image.jpg"
},
"carousel_room": 0,
"carousel_max": -9007199254740991,
"keep": [
"<keep>"
]
}
}Exchanges Instagram's code on the server, reads her profile and recent posts, and drops the token. Only for the login and shop that started the import, with the code the return page saw, once, within 10 minutes. Anything else is refused with LINK_EXPIRED (410) and the import is closed.
The shop (business auth checks the caller runs it).
The `code` from membber-business://instagram-import?…&outcome=returned&code=…, unchanged.
ok: open the picker (GET …/{importId}). Otherwise show that outcome's screen, as the web does.
curl -X POST "https://www.membber.com/api/v1/drops/instagram-import/e6d327a0-0000-4000-8000-d0c5000000e6/complete" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"code": "EXAMPLE10"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/instagram-import/{importId}/complete", {
params: { path: { importId: "e6d327a0-0000-4000-8000-d0c5000000e6" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
code: "EXAMPLE10"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.completeInstagramImport(
path: .init(importId: "e6d327a0-0000-4000-8000-d0c5000000e6"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
code: "EXAMPLE10"
))
).ok.body.json
print(response){
"import_id": "f3921ff5-0000-4000-8000-d0c5000000f3",
"outcome": "<outcome>"
}Copies it into the shop's own storage (the same place her uploads go). Films come in SILENT; a film over 60 seconds or 15 MB, or one that cannot be made silent safely, comes in as its cover. ONE video among her shop's own pictures: a film comes in as her video only while they hold none, so the first film that fits plays and every other comes in as its cover (`film_note: one_video`). With the video space rule on, a film is counted against the shop's video space like any video she uploads: no room (`video_space_full`, which also records her request for more), five video uploads of hers unfinished (`video_uploads_unfinished`) or a count that cannot be read (`video_space_unread`) and it comes in as its cover; the import never fails over it. Idempotent per piece.
The shop (business auth checks the caller runs it).
copiedalreadyimagevideosilentcover_onlyWhy a film came in as its cover: Instagram withheld it, over 60 seconds, over 15 MB, it could not be made silent safely, (one_video) her shop's own pictures already hold their one video, or, with the video space rule on, (video_space_full) her shop has no room for it, (video_uploads_unfinished) five video uploads of hers are started and not finished, or (video_space_unread) her video space could not be read just then.
withheldtoo_longtoo_bigcant_silenceone_videovideo_space_fullvideo_space_unreadvideo_uploads_unfinishedcurl -X POST "https://www.membber.com/api/v1/drops/instagram-import/e6d327a0-0000-4000-8000-d0c5000000e6/pieces/<key>" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/instagram-import/{importId}/pieces/{key}", {
params: { path: { importId: "e6d327a0-0000-4000-8000-d0c5000000e6", key: "<key>" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.copyInstagramPiece(
path: .init(importId: "e6d327a0-0000-4000-8000-d0c5000000e6", key: "<key>"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"key": "<key>",
"state": "copied",
"media": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"url": "https://example.com/image.jpg",
"media_type": "image",
"thumbnail_url": "https://example.com/image.jpg"
},
"film": "silent",
"film_note": "withheld"
}Writes the logo, name and bio at once and fixes the kept list. Refused with CAROUSEL_FULL past the shop's 10.
The shop (business auth checks the caller runs it).
Tile keys to bring in, in any order.
Use her Instagram picture as the shop's round logo.
The shop name, only if she changed it. 2 to 100 characters.
The shop bio, only if she changed it. At most 2,000 characters; empty clears it.
Copy each with POST …/pieces/{key}, one at a time, showing progress.
Her Instagram picture is under 200px: tell her it may look soft.
curl -X POST "https://www.membber.com/api/v1/drops/instagram-import/e6d327a0-0000-4000-8000-d0c5000000e6/save" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"keep": [
"<keep>"
],
"logo": true
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/instagram-import/{importId}/save", {
params: { path: { importId: "e6d327a0-0000-4000-8000-d0c5000000e6" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
keep: [
"<keep>"
],
logo: true
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.saveInstagramImport(
path: .init(importId: "e6d327a0-0000-4000-8000-d0c5000000e6"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
keep: ["<keep>"],
logo: true
))
).ok.body.json
print(response){
"import_id": "f3921ff5-0000-4000-8000-d0c5000000f3",
"status": "<status>",
"pieces": [
"<piece>"
],
"logo_url": "https://example.com/image.jpg",
"logo_small": true,
"shop": {
"name": "Example name",
"description": "Added at the front desk",
"logo_url": "https://example.com/image.jpg"
}
}Every Instagram button on web and in the Business app hides unless `available` is true.
The shop (business auth checks the caller runs it).
not_configured: no Meta app yet, hide every button. not_open_yet: testers only, and this shop is not one.
not_configurednot_open_yetHow many more photos or films the shop carousel can take (its limit is 10).
curl -G "https://www.membber.com/api/v1/drops/instagram-import/availability" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/instagram-import/availability", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getInstagramImportAvailability(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"available": true,
"reason": "not_configured",
"carousel_room": 0
}Public (the Business app sign-in sheet has no Membber session). Only an app import can be launched here; a web import never has a launch token. Sets an httpOnly binder cookie; the return is accepted only with the same cookie (login-CSRF protection), and an app import is then finished only by POST …/{importId}/complete under the login that started it.
curl -X POST "https://www.membber.com/api/v1/drops/instagram-import/launch" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"launch_token": "<launch_token>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/instagram-import/launch", {
body: {
launch_token: "<launch_token>"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.launchInstagramImport(
body: .json(.init(
launchToken: "<launch_token>"
))
).ok.body.json
print(response){
"authorize_url": "https://example.com"
}Form-encoded `signed_request`. Deletes everything that came from her Instagram: the imported photos and films, the logo and bio while still the imported ones, and every record of the link. Answers { url, confirmation_code }.
A page she can open to see that the deletion is done.
curl -X POST "https://www.membber.com/api/v1/drops/instagram/data-deletion" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/instagram/data-deletion", {
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.instagramDataDeletion().ok.body.json
print(response){
"url": "https://example.com",
"confirmation_code": "EXAMPLE10"
}Holds a question from someone with no account and emails them a code. PUBLIC. Nothing reaches the merchant and no customer record is created until the code is confirmed, so an unverified address cannot put a row in her book or a message in her inbox. One exception, chosen by the founder: an address already on this shop’s drop list with no existing thread posts straight away and receives a chat key, a drop signup already handed over that address, and a brand-new conversation exposes nothing that needs proof.
Where the code and the shop's replies go. Required, and exactly one address, or the request is refused.
curl -X POST "https://www.membber.com/api/v1/drops/message/<handle>" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"email": "alex@example.com",
"message": "Added at the front desk",
"name": "Example name"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/message/{handle}", {
params: { path: { handle: "<handle>" } },
body: {
email: "alex@example.com",
message: "Added at the front desk",
name: "Example name"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.startDropMessage(
path: .init(handle: "<handle>"),
body: .json(.init(
email: "alex@example.com",
message: "Added at the front desk",
name: "Example name"
))
).ok.body.json
print(response){
"ok": true,
"expires_at": "<expires_at>",
"conversation_id": "e701a257-0000-4000-8000-d0c5000000e7",
"chat_token": "<chat_token>"
}Requires the chat key issued when their emailed code was accepted. Marks the shop's replies as read.
Supplied by the URL PATH.
The key issued when their code was accepted.
ISO instant, poll for anything newer.
youshoptextimagevideofilesystemctacurl -G "https://www.membber.com/api/v1/drops/message/<handle>/chat" \
--data-urlencode "token=<token>"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.GET("/api/v1/drops/message/{handle}/chat", {
params: { path: { handle: "<handle>" }, query: { token: "<token>" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.readDropChat(
path: .init(handle: "<handle>"),
query: .init(token: "<token>")
).ok.body.json
print(response){
"messages": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"from": "you",
"body": "<body>",
"at": "<at>",
"read": true,
"kind": "text",
"mediaUrl": "https://example.com/image.jpg",
"mediaThumbnailUrl": "https://example.com/image.jpg",
"mediaWidth": 1,
"mediaHeight": 1,
"mediaMimeType": "<mediaMimeType>",
"mediaFileName": "<mediaFileName>",
"clientMessageId": "b36863f7-0000-4000-8000-d0c5000000b3"
}
]
}Requires the chat key. Posts into the same conversation the merchant already sees. With `client_message_id`, the send is deduped on it, exactly as the media send is, so a retry after a lost response posts once.
Supplied by the URL PATH.
The bubble identity. A retry with the same id returns the original message instead of posting a second copy. Optional so an older page still sends; the buyer page always sends one.
curl -X POST "https://www.membber.com/api/v1/drops/message/<handle>/chat" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"token": "<token>",
"message": "Added at the front desk"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/message/{handle}/chat", {
params: { path: { handle: "<handle>" } },
body: {
token: "<token>",
message: "Added at the front desk"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.sendDropChat(
path: .init(handle: "<handle>"),
body: .json(.init(
token: "<token>",
message: "Added at the front desk"
))
).ok.body.json
print(response){
"ok": true
}Sent as `multipart/form-data` with fields `file`, `token` and `client_message_id`. Requires the chat key, exactly as the text send does: the key names the conversation, so there is no id in the body for a caller to swap. Goes through the same upload path as the app: the per-shop block is checked BEFORE the bytes are buffered, only the types the bucket accepts are taken (anything else is a 415 with a readable line, never a 500), images are capped at 10MB and videos at 50MB, and the message is deduped on `client_message_id` so a double-tap posts once. Unlike the app upload this needs no complete customer profile, a drop buyer proves themselves with a code or a receipt, and the text send asks for nothing more either.
curl -X POST "https://www.membber.com/api/v1/drops/message/<handle>/chat/media" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/message/{handle}/chat/media", {
params: { path: { handle: "<handle>" } },
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.sendDropChatMedia(
path: .init(handle: "<handle>")
).ok.body.json
print(response){
"ok": true
}Emails a verification code for an address the page already holds, so somebody who signed up to the shop another way does not retype it to start a conversation. No question is attached: the code opens the chat and they type once inside. PUBLIC, and rate limited like the question route, because it sends mail to a caller-supplied address. An unexpired code for the same address is REUSED rather than re-sent, so reopening the sheet cannot burn the limit or leave the person reading an older email.
Where the code and the shop's replies go. Required, and exactly one address, or the request is refused.
curl -X POST "https://www.membber.com/api/v1/drops/message/<handle>/open" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"email": "alex@example.com",
"name": "Example name"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/message/{handle}/open", {
params: { path: { handle: "<handle>" } },
body: {
email: "alex@example.com",
name: "Example name"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.openDropChat(
path: .init(handle: "<handle>"),
body: .json(.init(
email: "alex@example.com",
name: "Example name"
))
).ok.body.json
print(response){
"ok": true,
"expires_at": "<expires_at>",
"reused": true
}Checks the emailed code and, only then, creates the lightweight customer and the conversation and posts the held question into the merchant's existing Messages tab. A wrong, missing or expired code all answer identically, so the endpoint cannot be used to discover whether a request is outstanding for an address.
Where the code and the shop's replies go. Required, and exactly one address, or the request is refused.
curl -X POST "https://www.membber.com/api/v1/drops/message/<handle>/verify" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"email": "alex@example.com",
"code": "EXAMPLE10"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/message/{handle}/verify", {
params: { path: { handle: "<handle>" } },
body: {
email: "alex@example.com",
code: "EXAMPLE10"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.verifyDropMessage(
path: .init(handle: "<handle>"),
body: .json(.init(
email: "alex@example.com",
code: "EXAMPLE10"
))
).ok.body.json
print(response){
"ok": true,
"conversation_id": "e701a257-0000-4000-8000-d0c5000000e7",
"chat_token": "<chat_token>"
}One row per money event (Sale, Refund, Dispute, Gift card sold, Gift card refund) in the shop's own zone, drops only. Every figure is recorded, "None" where it cannot exist, or "Not recorded"; nothing is estimated. No buyer names or contact details. 422 EXPORT_TOO_LARGE past 10,000 lines. Needs the can_view_reports permission.
UK tax periods in the shop's own zone: 6 April to 5 April, or the most recent COMPLETE standard quarter (6 Apr, 6 Jul, 6 Oct, 6 Jan). "custom" needs start_date and end_date; a preset refuses them.
this_tax_yearlast_tax_yearlast_tax_quartercustomcustom only: first shop-local day, inclusive.
custom only: last shop-local day, inclusive.
true brings back practice (Stripe test) card payments, each marked "Practice payment".
UTF-8 CSV text starting with a BOM, CRLF line endings
Of the CSV text as UTF-8 bytes, BOM included.
Shop-local first day actually covered.
Shop-local last day actually covered (never after today).
The IANA zone every date and time in the file is in.
When the request started; nothing after it is in the file.
Practice card payments and practice gift cards left out.
Drop orders placed in the period and not paid yet. Never rows.
Refund rows whose day is not recorded; the file says so on each.
Orders whose refund records do not match; those rows say so.
curl -G "https://www.membber.com/api/v1/drops/money/export" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066" \
--data-urlencode "period=this_tax_year"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/money/export", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066", period: "this_tax_year" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.exportDropsMoney(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066", period: "this_tax_year")
).ok.body.json
print(response){
"export": {
"filename": "<filename>",
"csv": "<csv>",
"sha256": "<sha256>",
"row_count": 1,
"sales": 0,
"refunds": 0,
"disputes": 0,
"gift_cards_sold": 0,
"gift_card_refunds": 0,
"from": "<from>",
"to": "<to>",
"timezone": "Europe/London",
"as_of": "2026-07-01T09:00:00Z",
"include_practice": true,
"left_out": {
"practice_payments": 0,
"unpaid_orders": 0
},
"undated_refunds": 0,
"mismatched_orders": 0
}
}Places an unpaid drop order priced exactly like a card order. The server checks the method is one the shop takes for this order, writes the order at awaiting_payment with the payee details and, for a bank transfer, the deadline, converts the basket's holds so the boxes are the buyer's from now, and returns the order page link. Public: the buyer has no account.
The drop line bought (the public drop's items[].id). Needed for an item sold in two or more sizes or colours; left out, the item's one line.
The price for one the buyer was shown, before the fee (the drop's items[].price_pence). Any difference from the current price is refused with 409 PRICE_CHANGED and the current prices.
The buyer's email. Optional: leave it out, or send null or an empty string, for no email. Anything else must be exactly one address, or the request is refused.
How the buyer will pay: a bank transfer before the deadline, or cash when they collect.
bank_transfercashbank_transfercashcurl -X POST "https://www.membber.com/api/v1/drops/order" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_code": "EXAMPLE10",
"idempotency_key": "1f0e2d3c-4b5a-4678-9abc-def012345678",
"batch_drop_id": "63a4cc66-0000-4000-8000-d0c500000063",
"cart_token": "<cart_token>",
"order_type": "<order_type>",
"collection_mode": "<collection_mode>",
"items": [
{
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"quantity": 1
}
],
"customer": {
"name": "Example name"
},
"payment_method": "bank_transfer"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/order", {
body: {
store_code: "EXAMPLE10",
idempotency_key: "1f0e2d3c-4b5a-4678-9abc-def012345678",
batch_drop_id: "63a4cc66-0000-4000-8000-d0c500000063",
cart_token: "<cart_token>",
order_type: "<order_type>",
collection_mode: "<collection_mode>",
items: [
{
menu_item_id: "ca65a6e7-0000-4000-8000-d0c5000000ca",
quantity: 1
}
],
customer: {
name: "Example name"
},
payment_method: "bank_transfer"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.placeDropOrder(
body: .json(.init(
storeCode: "EXAMPLE10",
idempotencyKey: "1f0e2d3c-4b5a-4678-9abc-def012345678",
batchDropId: "63a4cc66-0000-4000-8000-d0c500000063",
cartToken: "<cart_token>",
orderType: "<order_type>",
collectionMode: "<collection_mode>",
items: [.init(
menuItemId: "ca65a6e7-0000-4000-8000-d0c5000000ca",
quantity: 1
)],
customer: .init(
name: "Example name"
),
paymentMethod: .bankTransfer
))
).ok.body.json
print(response){
"order_id": "4991ffac-0000-4000-8000-d0c500000049",
"order_number": "<order_number>",
"status": "<status>",
"requires_payment": true,
"payment_method": "bank_transfer",
"order_page_path": "<order_page_path>",
"payment_due_at": "<payment_due_at>",
"collection_code": "EXAMPLE10",
"amount_pence": 1500,
"currency": "GBP",
"merchant_name": "<merchant_name>",
"replayed": true
}Everything the order page renders, from the placed row: the state line, the payee details snapshotted at placement, the deadline, the collection code once it is the buyer's, the items and the shop. Addressed by order number and the order page token; a wrong token is a 404. PUBLIC: a pay-later buyer has no account and no PaymentIntent.
The order page token from the placement, the text or the email.
bank_awaitingbank_overduebank_paidcash_awaitingcard_paidcollectedreleasednot_collectedrefundedcancelledpay_nowbank_transfercashcurl -G "https://www.membber.com/api/v1/drops/order-page" \
--data-urlencode "order_number=<order_number>" \
--data-urlencode "t=<t>"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.GET("/api/v1/drops/order-page", {
params: { query: { order_number: "<order_number>", t: "<t>" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.getDropOrderPage(
query: .init(orderNumber: "<order_number>", t: "<t>")
).ok.body.json
print(response){
"order_number": "<order_number>",
"state": "bank_awaiting",
"payment_method": "pay_now",
"total_pence": 1500,
"buyer_fee_pence": 1500,
"refunded_pence": 1500,
"promo_code": "EXAMPLE10",
"promo_discount_pence": 1500,
"currency": "GBP",
"collection_code": "EXAMPLE10",
"collect_scan_token": "<collect_scan_token>",
"payment_due_at": "<payment_due_at>",
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"payee": {
"account_name": "<account_name>",
"sort_code": "EXAMPLE10",
"account_number": "<account_number>",
"reference": "<reference>",
"amount_pence": 1500
},
"items": [
{
"name": "Example name",
"quantity": 1,
"line_total_pence": 1500
}
],
"collection": {
"date": "<date>",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"timezone": "Europe/London",
"address": "<address>"
},
"shop": {
"name": "Example name",
"code": "EXAMPLE10",
"handle": "<handle>",
"primary_color": "<primary_color>",
"logo_url": "https://example.com/image.jpg",
"contact_email": "alex@example.com",
"contact_phone": "+44 7700 900123"
},
"drop": {
"code": "EXAMPLE10",
"name": "Example name"
},
"buyer": {
"first_name": "Alex",
"phone_masked": "+44 7700 900123",
"email_masked": "alex@example.com"
},
"allergen_line": "<allergen_line>"
}Writes a UK mobile onto a paid order that has none, so the shop can reach the buyer about collection. Authorised by the PaymentIntent client_secret from the return URL. Only ever fills a blank: an order that already carries a number is left untouched and answers success.
curl -X POST "https://www.membber.com/api/v1/drops/order-phone" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"phone": "+44 7700 900123"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/order-phone", {
body: {
phone: "+44 7700 900123"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.addDropOrderPhone(
body: .json(.init(
phone: "+44 7700 900123"
))
).ok.body.json
print(response){
"ok": true,
"saved": true
}Creates the PaymentIntent for an anonymous drop purchase and returns its client secret, so the card is taken on the shop's own page rather than on a hosted Stripe page. PUBLIC: the buyer has no account. Deliberately the SAME service call the customer app uses, so stock banking, receipts and the sold-out auto-refund all keep hanging off payment_intent.succeeded.
The drop line bought (the public drop's items[].id). Needed for an item sold in two or more sizes or colours; left out, the item's one line.
The price for one the buyer was shown, before the fee (the drop's items[].price_pence). Any difference from the current price is refused with 409 PRICE_CHANGED and the current prices.
The buyer's email. Optional: leave it out, or send null or an empty string, for no email. Anything else must be exactly one address, or the request is refused.
destinationdirectcurl -X POST "https://www.membber.com/api/v1/drops/payment" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_code": "EXAMPLE10",
"idempotency_key": "1f0e2d3c-4b5a-4678-9abc-def012345678",
"batch_drop_id": "63a4cc66-0000-4000-8000-d0c500000063",
"cart_token": "<cart_token>",
"order_type": "<order_type>",
"collection_mode": "<collection_mode>",
"items": [
{
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"quantity": 1
}
],
"customer": {
"name": "Example name"
}
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/payment", {
body: {
store_code: "EXAMPLE10",
idempotency_key: "1f0e2d3c-4b5a-4678-9abc-def012345678",
batch_drop_id: "63a4cc66-0000-4000-8000-d0c500000063",
cart_token: "<cart_token>",
order_type: "<order_type>",
collection_mode: "<collection_mode>",
items: [
{
menu_item_id: "ca65a6e7-0000-4000-8000-d0c5000000ca",
quantity: 1
}
],
customer: {
name: "Example name"
}
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.startDropPayment(
body: .json(.init(
storeCode: "EXAMPLE10",
idempotencyKey: "1f0e2d3c-4b5a-4678-9abc-def012345678",
batchDropId: "63a4cc66-0000-4000-8000-d0c500000063",
cartToken: "<cart_token>",
orderType: "<order_type>",
collectionMode: "<collection_mode>",
items: [.init(
menuItemId: "ca65a6e7-0000-4000-8000-d0c5000000ca",
quantity: 1
)],
customer: .init(
name: "Example name"
)
))
).ok.body.json
print(response){
"order_id": "4991ffac-0000-4000-8000-d0c500000049",
"order_number": "<order_number>",
"status": "<status>",
"requires_payment": true,
"client_secret": "<client_secret>",
"connected_account_id": "231b6f63-0000-4000-8000-d0c500000023",
"publishable_key": "<publishable_key>",
"charge_model": "destination",
"amount_pence": 1500,
"currency": "GBP",
"merchant_name": "<merchant_name>",
"order_page_path": "<order_page_path>"
}Merchant, her own store. In one transaction: refused unless the caller is the account that verified the code (409 NOT_VERIFIED) and the preview is still unclaimed (409 PREVIEW_CLAIMED); the photos she kept are copied into her own storage and become her shop photos (and her logo, only if she has none), with the licence recorded in the consent ledger; the private originals are deleted. Never creates menu items or a drop. A retry by the same account for the same store answers the same.
Her own shop (the one the Drops signup made, or already had).
The preview photos she ticked "keep" (her logo and product photos). Empty: start without them.
The photo licence version the page showed her. Required when keep_photo_ids is not empty.
curl -X POST "https://www.membber.com/api/v1/drops/previews/<token>/claim/complete" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"keep_photo_ids": [
"11a759c2-0000-4000-8000-d0c500000011"
]
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/previews/{token}/claim/complete", {
params: { path: { token: "<token>" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
keep_photo_ids: [
"11a759c2-0000-4000-8000-d0c500000011"
]
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.dropsPreviewClaimComplete(
path: .init(token: "<token>"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
keepPhotoIds: ["11a759c2-0000-4000-8000-d0c500000011"]
))
).ok.body.json
print(response){
"claimed": true,
"proof_code": "EXAMPLE10",
"next": "<next>",
"photos_kept": 1,
"shop": {
"name": "Example name",
"drops_link": "<drops_link>"
},
"instagram_href": "<instagram_href>"
}Public: the private link is the key. Sends a code valid for 15 minutes to the address; a new request supersedes the last code. Rate limited per IP, per link and per address. 404 when the link is unknown, deleted or past its 30 days; 409 PREVIEW_CLAIMED once it has been claimed.
Her shop name, pre-filled from the preview and editable.
Her full name (the Drops signup asks the same).
The visit ticket her page was given when it loaded. Opaque; one per page load.
curl -X POST "https://www.membber.com/api/v1/drops/previews/<token>/claim/start" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"shop_name": "<shop_name>",
"owner_name": "<owner_name>",
"email": "alex@example.com"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/previews/{token}/claim/start", {
params: { path: { token: "<token>" } },
body: {
shop_name: "<shop_name>",
owner_name: "<owner_name>",
email: "alex@example.com"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.dropsPreviewClaimStart(
path: .init(token: "<token>"),
body: .json(.init(
shopName: "<shop_name>",
ownerName: "<owner_name>",
email: "alex@example.com"
))
).ok.body.json
print(response){
"sent": true,
"expires_at": "<expires_at>",
"resend_after_sec": 1
}Public. Checks the code in constant time (5 tries, then it is locked; 15 minutes). Creates the account with the email confirmed and no password, or signs in the account that already has this address. An admin account is refused (403 USE_YOUR_LOGIN). Codes: 422 CODE_WRONG (with attempts_left), 410 CODE_EXPIRED, 423 CODE_LOCKED, 404 NO_CODE, 409 PREVIEW_CLAIMED.
curl -X POST "https://www.membber.com/api/v1/drops/previews/<token>/claim/verify" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"email": "alex@example.com",
"code": "EXAMPLE10"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/previews/{token}/claim/verify", {
params: { path: { token: "<token>" } },
body: {
email: "alex@example.com",
code: "EXAMPLE10"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.dropsPreviewClaimVerify(
path: .init(token: "<token>"),
body: .json(.init(
email: "alex@example.com",
code: "EXAMPLE10"
))
).ok.body.json
print(response){
"access_token": "<access_token>",
"refresh_token": "<refresh_token>",
"user_id": "f73aee0f-0000-4000-8000-d0c5000000f7",
"new_account": true,
"existing_shop": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"drops": true
},
"attribution": {
"utm_source": "<utm_source>",
"utm_medium": "<utm_medium>",
"utm_campaign": "<utm_campaign>",
"from_page": "<from_page>",
"landing_path": "<landing_path>",
"method": "<method>"
}
}Public, called by the private preview page itself (a server visit is not an open: Meta checks every link in a DM). opened is kept once per visit (each page load is a visit; a retry is not) and bumps the open count; make_it_mine_tapped is written once per preview and noted in her visit. `visit` is required: a post without one is a 400 and records nothing. A crawler user agent, an unknown link, an expired one and a visit ticket that is not good for the link all answer recorded: false (an open with a bad ticket is never counted).
opened: the page loaded and was on screen. make_it_mine_tapped: she tapped Make it mine.
openedmake_it_mine_tappedThe visit ticket her page was given when it loaded. Opaque; one per page load.
curl -X POST "https://www.membber.com/api/v1/drops/previews/<token>/events" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"event": "opened",
"visit": "<visit>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/previews/{token}/events", {
params: { path: { token: "<token>" } },
body: {
event: "opened",
visit: "<visit>"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.dropsPreviewPageEvent(
path: .init(token: "<token>"),
body: .json(.init(
event: .opened,
visit: "<visit>"
))
).ok.body.json
print(response){
"recorded": true
}Public, no login, while her setup is not open yet. With no body it records her first tap on I'm interested (later taps change nothing) and tells the founder. With `contact` it keeps the email address or phone number she typed (once; a phone number is kept in E.164), only to reply to her about her preview and setting up her shop. 400 INVALID_CONTACT when it is neither; 404 for an unknown, expired or draft link; 409 PREVIEW_CLAIMED once she has claimed it. Her contact is never returned.
Her email address or phone number, as she typed it after tapping I'm interested. Omitted on the tap itself.
The visit ticket her page was given when it loaded. Opaque; one per page load.
curl -X POST "https://www.membber.com/api/v1/drops/previews/<token>/interested" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"contact": "<contact>",
"visit": "<visit>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/previews/{token}/interested", {
params: { path: { token: "<token>" } },
body: {
contact: "<contact>",
visit: "<visit>"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.dropsPreviewInterested(
path: .init(token: "<token>"),
body: .json(.init(
contact: "<contact>",
visit: "<visit>"
))
).ok.body.json
print(response){
"contact_given": true
}Public, called by the private preview page itself, never by its admin or review views. Notes one part she opened (her menu, which item, the drop, More info, or Be the first to know) against her visit: each part once per visit, each item once per item per visit. No cookie, no device detail, nothing she typed; deleted with the preview, and when she claims it. `visit` is required: a post without one is a 400 and records nothing. A crawler user agent, a visit ticket that is not good for the link and an item that is not this preview's answer recorded: false; 404 for an unknown, expired, draft or claimed link.
What she opened: menu (her menu sheet), item (an item, named by `item`), drop (the drop, from its card or page), more_info (More info under her photo), first_to_know (she tapped Be the first to know).
menuitemdropmore_infofirst_to_knowThe visit ticket her page was given when it loaded. Opaque; one per page load.
Which item she opened (part item only): a reference her page was given, never the item name.
curl -X POST "https://www.membber.com/api/v1/drops/previews/<token>/looked-at" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"part": "menu",
"visit": "<visit>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/previews/{token}/looked-at", {
params: { path: { token: "<token>" } },
body: {
part: "menu",
visit: "<visit>"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.dropsPreviewLook(
path: .init(token: "<token>"),
body: .json(.init(
part: .menu,
visit: "<visit>"
))
).ok.body.json
print(response){
"recorded": true
}Public, one tap, no login. In one transaction her handle goes on the do-not-contact list and the preview is deleted; her photos then leave storage. 404 when the link is unknown or already deleted; 409 PREVIEW_CLAIMED once she has claimed it.
curl -X POST "https://www.membber.com/api/v1/drops/previews/<token>/not-for-me" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/previews/{token}/not-for-me", {
params: { path: { token: "<token>" } },
body: {},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.dropsPreviewNotForMe(
path: .init(token: "<token>"),
body: .json(.init())
).ok.body.json
print(response){
"deleted": true,
"receipt": "40827238-0000-4000-8000-d0c500000040"
}Public. Records one of three reasons (not selling now, happy with DMs, use something else) on the not-for-me event, once, within 10 minutes of the delete. 404 when it is too late or the receipt is not one.
The receipt the not-for-me call returned.
not_selling_nowhappy_with_dmsuse_something_elsecurl -X POST "https://www.membber.com/api/v1/drops/previews/not-for-me-reason" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"receipt": "40827238-0000-4000-8000-d0c500000040",
"reason": "not_selling_now"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/previews/not-for-me-reason", {
body: {
receipt: "40827238-0000-4000-8000-d0c500000040",
reason: "not_selling_now"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.dropsPreviewNotForMeReason(
body: .json(.init(
receipt: "40827238-0000-4000-8000-d0c500000040",
reason: .notSellingNow
))
).ok.body.json
print(response){
"saved": true
}Public. Prices the basket with and without the code through the same function the order is charged by, and returns the saving, the booking fee and the new total. Read-only: nothing is reserved until the order is placed. A code that does not exist, is limited to another drop, or is paused all read the same.
The drop line bought (the public drop's items[].id). Needed for an item sold in two or more sizes or colours; left out, the item's one line.
The price for one the buyer was shown, before the fee (the drop's items[].price_pence). Any difference from the current price is refused with 409 PRICE_CHANGED and the current prices.
pay_nowbank_transfercashThe buyer's email. Optional: leave it out, or send null or an empty string, for no email. Anything else must be exactly one address, or the request is refused.
curl -X POST "https://www.membber.com/api/v1/drops/promo" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_code": "EXAMPLE10",
"batch_drop_id": "63a4cc66-0000-4000-8000-d0c500000063",
"cart_token": "<cart_token>",
"order_type": "<order_type>",
"collection_mode": "<collection_mode>",
"items": [
{
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"quantity": 1
}
],
"promo_code": "EXAMPLE10",
"payment_method": "pay_now"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/promo", {
body: {
store_code: "EXAMPLE10",
batch_drop_id: "63a4cc66-0000-4000-8000-d0c500000063",
cart_token: "<cart_token>",
order_type: "<order_type>",
collection_mode: "<collection_mode>",
items: [
{
menu_item_id: "ca65a6e7-0000-4000-8000-d0c5000000ca",
quantity: 1
}
],
promo_code: "EXAMPLE10",
payment_method: "pay_now"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.previewDropPromo(
body: .json(.init(
storeCode: "EXAMPLE10",
batchDropId: "63a4cc66-0000-4000-8000-d0c500000063",
cartToken: "<cart_token>",
orderType: "<order_type>",
collectionMode: "<collection_mode>",
items: [.init(
menuItemId: "ca65a6e7-0000-4000-8000-d0c5000000ca",
quantity: 1
)],
promoCode: "EXAMPLE10",
paymentMethod: .payNow
))
).ok.body.json
print(response){
"promo_code": "EXAMPLE10",
"promo_label": "<promo_label>",
"goods_pence": 1500,
"discount_pence": 1500,
"buyer_fee_pence": 1500,
"total_pence": 1500,
"total_before_pence": 1500,
"saving_pence": 1500
}Public. Everything a buyer needs to decide: what is in it, how many are left, when it opens, when to collect, and the allergens the seller declared. Returns the drop even before it opens so the page can count down; the menu itself is withheld while the seller has chosen to hide it.
pay_nowbank_transfercashimage | video
Still frame for a film.
Present (false) when she holds her drop's header still for every buyer. Absent: it moves on by itself.
scheduledliveclosedcollectedcancelledHow many boxes are not free only because they are in other people's baskets right now. total minus remaining minus this is how many boxes have sold. Null when the shop hides counts.
The item’s name; on a line that sells a size or colour, with them: "Hoodie, M, Black".
Her item’s own name, on a line that sells one of its sizes or colours (`name` then reads "Hoodie, M, Black").
The size this line sells, as her item names it. Absent: no size.
The colour this line sells, as her item names it. Absent: no colour.
Her own word for this item’s sizes ("Flavour"), when she chose one. Absent: the word for size.
Her own word for this item’s colours ("Grind"), when she chose one. Absent: the word for colour.
The photo she chose for this colour, one of the item’s own photos. Absent: none chosen.
Her item's photos and videos, in her order (a video may come first), for a carousel: present only when there is more than her cover to show. Absent: the item is drawn from `image_url` alone, as before. `image_url` is always a photo.
image | video
Still frame for a film.
Present (false) when she holds her item's photos and videos still. Absent: they move on by themselves.
False for an item that is not food (clothes, flowers): no allergen line is drawn for it, and `ingredients` is what it is made of. True or absent: food.
Their shop name, which is their own address: {handle}.membber.com
fill = cover the circle; fit = the whole logo inside, on `disc`.
fillfitThe point of the logo at the circle centre, across (0..1).
The point of the logo at the circle centre, down (0..1).
1 = the short side spans the circle.
#RRGGBB behind the logo.
The logo file's width / height.
The merchant's own banner artwork.
The trader as the law knows them, printed as "{name}, run by {legal_name}".
The business email the law wants on the page, shown as a mailto link.
Printed as "VAT {vat_number}" only when the trader is VAT registered.
plain | brand | color | image
#RRGGBB when color, a URL when image
solidstripesTrue only when a drop order here will earn a stamp at collection (loyalty_on_drops, off by default).
curl -G "https://www.membber.com/api/v1/drops/public/EXAMPLE10"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.GET("/api/v1/drops/public/{dropCode}", {
params: { path: { dropCode: "EXAMPLE10" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.getPublicBatchDrop(
path: .init(dropCode: "EXAMPLE10")
).ok.body.json
print(response){
"drop": {
"stripe": {
"publishable_key": "<publishable_key>",
"connected_account_id": "231b6f63-0000-4000-8000-d0c500000023",
"currency": "GBP"
},
"payment_methods": [
"pay_now"
],
"id": "00000d1b-0000-4000-8000-d0c500000000",
"code": "EXAMPLE10",
"name": "Example name",
"description": "Added at the front desk",
"image_url": "https://example.com/image.jpg",
"gallery": [
{
"url": "https://example.com/image.jpg",
"type": "<type>",
"title": "Morning class",
"poster": "<poster>"
}
],
"header_auto_advance": true,
"state": "scheduled",
"opens_at": "<opens_at>",
"closes_at": "<closes_at>",
"collection_date": "<collection_date>",
"timezone": "Europe/London",
"checkout_ttl_seconds": 1,
"max_per_customer": 1,
"buyer_fee_percent": 1,
"bundle": {
"price_pence": 1500,
"buyer_price_pence": 1500,
"total": -9007199254740991,
"remaining": -9007199254740991,
"in_other_baskets": -9007199254740991,
"sold_out": true
},
"items": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"per_bundle": -9007199254740991,
"name": "Example name",
"item_name": "<item_name>",
"size_label": "<size_label>",
"colour_label": "<colour_label>",
"size_name": "<size_name>",
"colour_name": "<colour_name>",
"colour_photo_url": "https://example.com/image.jpg",
"description": "Added at the front desk",
"price_pence": 1500,
"buyer_price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"media": [
{
"url": "https://example.com/image.jpg",
"type": "<type>",
"title": "Morning class",
"poster": "<poster>"
}
],
"media_auto_advance": true,
"sold_out": true,
"remaining": 1,
"in_other_baskets": 1,
"max_per_order": 1,
"batch_total": 1,
"category": "<category>",
"emoji": "<emoji>",
"dietary_tags": [
"<dietary_tag>"
],
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"may_contain_allergens": [
"<may_contain_allergen>"
],
"allergen_note": "Added at the front desk",
"ingredients": "<ingredients>",
"is_food": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"full": true,
"places_left": -9007199254740991
}
],
"store": {
"code": "EXAMPLE10",
"name": "Example name",
"handle": "<handle>",
"logo_url": "https://example.com/image.jpg",
"logo_display": {
"mode": "fill",
"x": 0,
"y": 0,
"scale": 1,
"disc": "<disc>",
"aspect": 1
},
"banner_url": "https://example.com/image.jpg",
"wordmark_url": "https://example.com/image.jpg",
"texture_url": "https://example.com/image.jpg",
"primary_color": "<primary_color>",
"secondary_color": "<secondary_color>",
"accent_color": "<accent_color>",
"description": "Added at the front desk",
"address": "<address>",
"legal_name": "<legal_name>",
"email": "alex@example.com",
"vat_number": "<vat_number>",
"instagram": "<instagram>",
"facebook": "<facebook>",
"tiktok": "<tiktok>",
"bg_mode": "<bg_mode>",
"bg_value": "<bg_value>",
"ribbon_style": "solid",
"cta_label": "<cta_label>",
"cta_color": "<cta_color>",
"empty_title": "<empty_title>",
"empty_body": "<empty_body>",
"earns_stamps": true
}
}
}Public. Holds stock the moment it goes in the basket, not at payment, and starts the countdown. Sets the basket to the given quantity rather than adding to it, so a retry or a double tap converges instead of stacking. Send 0 to remove. Returns which limit bound if you got less than you asked for.
nonestockmax_per_ordermax_per_customerwindow_fullwindow_not_foundreleasedcurl -X POST "https://www.membber.com/api/v1/drops/public/EXAMPLE10/hold" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"cart_token": "<cart_token>",
"item_id": "7e22b9c7-0000-4000-8000-d0c50000007e",
"quantity": 1
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/public/{dropCode}/hold", {
params: { path: { dropCode: "EXAMPLE10" } },
body: {
cart_token: "<cart_token>",
item_id: "7e22b9c7-0000-4000-8000-d0c50000007e",
quantity: 1
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.holdBatchDropCart(
path: .init(dropCode: "EXAMPLE10"),
body: .json(.init(
cartToken: "<cart_token>",
itemId: "7e22b9c7-0000-4000-8000-d0c50000007e",
quantity: 1
))
).ok.body.json
print(response){
"granted": 1,
"bound_by": "none",
"remaining": 1,
"expires_at": "<expires_at>"
}Public. Releases everything this basket holds, so it goes back on sale immediately rather than waiting for the countdown. Called by the drop page when it ends a basket at the end of its hold. A card payment prepared for this basket that has not gone through is cancelled first; one that has gone through, is going through or holds money is never cancelled, and then nothing is released (released is 0): its holds end with their own time.
curl -X POST "https://www.membber.com/api/v1/drops/public/EXAMPLE10/release" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"drop_code": "EXAMPLE10",
"cart_token": "<cart_token>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/public/{dropCode}/release", {
params: { path: { dropCode: "EXAMPLE10" } },
body: {
drop_code: "EXAMPLE10",
cart_token: "<cart_token>"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.releaseBatchDropCart(
path: .init(dropCode: "EXAMPLE10"),
body: .json(.init(
dropCode: "EXAMPLE10",
cartToken: "<cart_token>"
))
).ok.body.json
print(response){
"released": 1
}Sets a per-drop reminder: the day before it opens, at open, and before it closes. PUBLIC, no account needed. Does NOT join the shop's general list; a second submission re-confirms rather than duplicating.
An email to be told on, instead of or as well as a mobile. Optional: leave it out, or send null or an empty string, for no email. Anything else must be exactly one address, or the request is refused.
phoneemailcurl -X POST "https://www.membber.com/api/v1/drops/remind/EXAMPLE10" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"phone": "+44 7700 900123",
"email": "alex@example.com",
"consent_text": "<consent_text>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/remind/{dropCode}", {
params: { path: { dropCode: "EXAMPLE10" } },
body: {
phone: "+44 7700 900123",
email: "alex@example.com",
consent_text: "<consent_text>"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.remindAboutDrop(
path: .init(dropCode: "EXAMPLE10"),
body: .json(.init(
phone: "+44 7700 900123",
email: "alex@example.com",
consentText: "<consent_text>"
))
).ok.body.json
print(response){
"ok": true,
"via": "phone",
"already": true,
"on_list": true
}PUBLIC, authorised entirely by the emailed token. Called twice for one review by design: the star tapped in the email records the rating, then the page adds words to the SAME row. The underlying function refuses an order that was never collected and a token that does not exist, so a review cannot exist without a purchase behind it.
nameanonymouscurl -X POST "https://www.membber.com/api/v1/drops/review" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"token": "0696b9f9-0000-4000-8000-d0c500000006",
"rating": 1,
"display_mode": "name"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/review", {
body: {
token: "0696b9f9-0000-4000-8000-d0c500000006",
rating: 1,
display_mode: "name"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.submitDropReview(
body: .json(.init(
token: "0696b9f9-0000-4000-8000-d0c500000006",
rating: 1,
displayMode: .name
))
).ok.body.json
print(response){
"review_id": "1d631b62-0000-4000-8000-d0c50000001d",
"rating": 1,
"has_body": true,
"items": [
"<item>"
]
}The shops the signed-in business user can run drops for, and for the chosen one (store_id if she may act for it, else the first): its drops (as GET /api/v1/drops), its setup and go-live list (as GET /api/ios/settings), its logo and its shop link. Requires a Business Bearer token. A shop that holds no Drops add-on is refused with ENTITLEMENT_DENIED, exactly as the drops list refuses it.
The shop to open on (the one this browser had open last). Used only if the caller may act for it; otherwise the first shop is opened.
Every shop she can run drops for. Empty is a real answer.
The store code, which photo uploads address a store by.
The shop's own clock. Every drop time is in this zone.
True when the shop came in through the Drops door and sells drops only (`dropsOnlyStoreIds`, the predicate that gives it the short go-live list). The browser uses it to open on the Drops setup list and to land Log in on the seller area. It never grants anything.
The shop's logo, or null until she adds one.
How her logo sits in its circle, as buyers see it (Drops shop setup P2). Null with no logo, or until it is worked out.
fill = cover the circle; fit = the whole logo inside, on `disc`.
fillfitThe point of the logo at the circle centre, across (0..1).
The point of the logo at the circle centre, down (0..1).
1 = the short side spans the circle.
#RRGGBB behind the logo.
The logo file's width / height.
The shop's hidden test switch (`stores.is_test`). True draws the one TEST chip beside the mark, and nothing else on any screen talks about tests (DESIGN.md "A screen explains itself"). False when unreadable.
What she sells: a Drops kind page slug or "something-else", her own choice else the kind page she signed up from. Null or absent: unknown, read as food. Her catalogue is called Menu for food and Items otherwise.
The shop opened. Null only when there are no shops.
The opened shop's drops, exactly as GET /api/v1/drops returns them.
The short code that identifies this drop.
The shop's permanent page, at its own address: https://{handle}.membber.com/
The link to this specific drop.
draftscheduledcancelledComputed at read time. Never sent by a client.
draftscheduledliveclosedcollectedcancelledNull only while a draft has no closing time yet. A scheduled drop always has one.
Copied from the menu item at read time, never stored twice. On a line that sells a size or colour: the item’s name with them, "Hoodie, M, Black".
Her item’s own name, on a line that sells one of its sizes or colours (`name` then reads "Hoodie, M, Black").
The size this line sells, as her item names it. Absent: no size.
The colour this line sells, as her item names it. Absent: no colour.
Her own word for this item’s sizes ("Flavour"), when she chose one. Absent: the word for size.
Her own word for this item’s colours ("Grind"), when she chose one. Absent: the word for colour.
The photo she chose for this colour, one of the item’s own photos. Absent: none chosen.
What one sells at in this drop, before Membber's fee: the drop's own price when it has one, else the menu price. What checkout charges.
Her usual (menu) price for one.
This drop's own price for one, or null when it sells at the usual price.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
How many of this item go in one box of a one-price drop. Null means one.
Always null for an unlimited item.
False for an item that is not food (clothes, flowers): no allergen answer is asked of it. True or absent: food.
Send it back on the windows PUT to edit this window in place.
null = no cap on orders in this window.
People still to collect from this window. Moving it needs confirm_booked_changes; removing it is refused.
First names of the people still to collect, oldest order first.
False when anyone holds a place in this window, collected or not: removing it is refused.
True when the only orders holding this window are disputed ones nobody collected. Absent otherwise.
Buyers still to collect whom a moved collection time could not reach. Absent when there are none.
How many buyers could not be reached about a moved time.
Their first names, oldest order first, three at most (the most the sentence names). Shorter than not_told when one gave no name or there are more than three; absent when none gave one.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Orders refunded in full, disputed by the buyer, released unpaid or cancelled with the drop, newest first. Listed only: never in buyers and never in any count or total.
Why a cancelled order ended, e.g. released_unpaid, drop_cancelled, dispute_created or dispute_lost. Absent on buyers.
On a disputed order: when the dispute must be answered by, from Stripe (ISO instant). Null when the order is not disputed or the deadline was not recorded.
The part of refunded_pence a lost dispute took: the buyer's bank took it back, the shop did not refund it. Null when none was lost or it was not recorded.
Sum of the counted items only; an unlimited item adds nothing.
TAKEN: money that has arrived and stayed, from every paid status (paid, ready, collected, part-refunded), net of recorded refunds. Card orders without the buyer fee; cash and transfer in full.
How many boxes were put up.
How many boxes are left. Stock, never writable.
True when this drop sells one box at one price.
no_itemsno_timesno_closesno_quantityno_priceno_allergenstime_before_openstime_before_closestime_off_dayWhy a drop cannot open. Say this to the merchant, do not refuse blankly.
no_addoncollect_offno_paymentspayments_destinationunavailableWhether the shop is live. A drop can only be scheduled on a live shop. Null when unreadable.
Pay now is on but cards are not set up yet: this real shop is still on its old practice Stripe account, which takes no card. "Set up cards" makes its live account.
pay_nowbank_transfercashVisits to this shop's public drops pages. Null only when the count could not be read.
True while downloads are switched on: a seller screen then shows its Downloads row. Absent otherwise.
The opened shop's setup. Null when it could not be read this time: the screens then read it themselves.
The go-live list: the same rows GET /api/ios/settings shows and the go-live gate enforces.
Whether the shop is open to buyers.
Whether every required row is done, so going live would be accepted.
When her two weeks' grace runs out (`stores.listing_grace_until`), as an ISO instant, past or future. Null when she has none. Read only: the server works it out, and no request can set it. The setup list says one line with it to a LIVE shop with a required row not done: "Finish these by 19 Oct to stay on Google and AI search." while it is ahead, "Finish these to be found on Google and AI search." once it has passed.
The address people collect from.
What signup's "Your full name" stored. Sent only to the owner herself; null for a team member.
The email she signs in with.
The shop phone number, as she wrote it. Null until she adds one.
Her switch for showing that number on her shop page (`stores.phone_public`). Off unless she turned it on; only an owner may change it, through PATCH /api/ios/settings.
The shop's permanent page, at its own address (https://{handle}.membber.com/). Null until a handle exists or when unreadable.
Present (true) only while the video space rule is switched on: her sheets may then ask what she has used (GET /api/v1/drops/shop/videos). Left out while it is off, and nothing is asked.
Present (true) only while shrinking videos in the seller's browser is switched on: a video she adds is then made 720p H.264 MP4 on her own device before it is sent. Left out while it is off, and the file she picked goes as it is.
curl -G "https://www.membber.com/api/v1/drops/seller-home" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/seller-home");
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getDropsSellerHome().ok.body.json
print(response){
"stores": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"code": "EXAMPLE10",
"timezone": "Europe/London",
"drops_only": true,
"logo_url": "https://example.com/image.jpg",
"logo_display": {
"mode": "fill",
"x": 0,
"y": 0,
"scale": 1,
"disc": "<disc>",
"aspect": 1
},
"is_test": true,
"sells": "<sells>"
}
],
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"drops": {
"drops": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"code": "EXAMPLE10",
"shop_url": "https://example.com/image.jpg",
"image_url": "https://example.com/image.jpg",
"share_url": "https://example.com/image.jpg",
"name": "Example name",
"description": "Added at the front desk",
"status": "draft",
"state": "draft",
"opens_at": "<opens_at>",
"closes_at": "<closes_at>",
"collection_date": "<collection_date>",
"timezone": "Europe/London",
"checkout_ttl_seconds": 1,
"menu_visible_before_open": true,
"show_stock_count": true,
"max_per_customer": 1,
"items": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"item_name": "<item_name>",
"size_label": "<size_label>",
"colour_label": "<colour_label>",
"size_name": "<size_name>",
"colour_name": "<colour_name>",
"colour_photo_url": "https://example.com/image.jpg",
"price_pence": 1500,
"usual_price_pence": 1500,
"drop_price_pence": 1500,
"buyer_price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"per_bundle": -9007199254740991,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
null
],
"allergens_declared_none": true,
"allergens_missing": true,
"is_food": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1,
"booked_count": 1,
"booked_names": [
null
],
"removable": true,
"in_dispute": true
}
],
"moves_not_reached": {
"not_told": -9007199254740991,
"not_told_names": [
"<not_told_name>"
]
},
"buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"cancelled_buyers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"name": "Example name",
"email": "alex@example.com",
"total_pence": 1500,
"paid_at": "<paid_at>",
"collected_at": "<collected_at>",
"fulfilment_status": "<fulfilment_status>",
"collection_at": "<collection_at>",
"status": "<status>",
"payment_method_kind": "<payment_method_kind>",
"offline_payment_due_at": "<offline_payment_due_at>",
"offline_payment_recorded_at": "<offline_payment_recorded_at>",
"buyer_fee_pence": 1500,
"application_fee_pence": 1500,
"refunded_pence": 1500,
"stripe_mode": "<stripe_mode>",
"added_by_shop": true,
"failure_code": "EXAMPLE10",
"dispute_due_by": "<dispute_due_by>",
"dispute_lost_pence": 1500
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"still_to_come_pence": 1500,
"unpaid_orders_count": 1,
"bundle_price_pence": 1500,
"bundle_buyer_price_pence": 1500,
"bundle_total": 1,
"bundle_remaining": 1,
"is_bundle": true,
"blockers": [
"<blocker>"
],
"blocker_codes": [
{
"code": "no_items",
"names": [
null
]
}
],
"making_started_at": "<making_started_at>",
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
],
"live_drop_id": "68f0d418-0000-4000-8000-d0c500000068",
"drops_live": true,
"blocked_reason": "no_addon",
"shop_live": true,
"card_practice_only": true,
"payment_methods": [
"pay_now"
],
"reachable_customers": -9007199254740991,
"page_views": {
"shop_today": -9007199254740991,
"shop_last_7_days": -9007199254740991,
"drops_today": -9007199254740991,
"drops_last_7_days": -9007199254740991
},
"downloads_enabled": true
},
"setup": {
"requirements": [
{
"key": "<key>",
"label": "<label>",
"complete": true,
"required": true
}
],
"is_live": true,
"can_go_live": true,
"listing_grace_until": "<listing_grace_until>",
"address": "<address>",
"legal_name": "<legal_name>",
"business_email": "alex@example.com",
"owner_name": "<owner_name>",
"account_email": "alex@example.com",
"phone": "+44 7700 900123",
"phone_public": true
},
"shop_url": "https://example.com/image.jpg",
"video_space": true,
"video_compress": true
}For the shop's messages inside Buyers: for each conversation id given (at most 50, all of this shop's), the person it is with and the name and email this shop was given for them, so her buyers list, the buyer page and the chat say one name. Owner, or staff allowed to message customers.
The conversations on screen, comma separated, at most 50.
One per conversation of this shop that has a person. Another shop's conversation is simply absent.
The shop's own name for them, else the latest name typed at her checkout, else the first name they gave Ask the shop. Null when she was never given one. Never the name on their shared account.
Only an email this shop was given. The title when there is no name, as on her customers list.
Only a mobile this shop was given, and only when it has no name and no email for them: then it is the title. Null otherwise.
curl -G "https://www.membber.com/api/v1/drops/seller-messages/people" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066" \
--data-urlencode "conversation_ids=<conversation_ids>"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/seller-messages/people", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066", conversation_ids: "<conversation_ids>" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.listDropsSellerMessagePeople(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066", conversationIds: "<conversation_ids>")
).ok.body.json
print(response){
"people": [
{
"conversation_id": "e701a257-0000-4000-8000-d0c5000000e7",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"name": "Example name",
"email": "alex@example.com",
"phone": "+44 7700 900123"
}
]
}Every store the signed-in business user may act for that holds the Membber Drops add-on, with its timezone. Requires a Business Bearer token. This only decides which shops to OFFER: every drops route still checks the add-on itself.
The store code, which photo uploads address a store by.
The shop's own clock. Every drop time is in this zone.
True when the shop came in through the Drops door and sells drops only (`dropsOnlyStoreIds`, the predicate that gives it the short go-live list). The browser uses it to open on the Drops setup list and to land Log in on the seller area. It never grants anything.
curl -G "https://www.membber.com/api/v1/drops/seller-stores" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/seller-stores");
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.listDropsSellerStores().ok.body.json
print(response){
"stores": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"code": "EXAMPLE10",
"timezone": "Europe/London",
"drops_only": true
}
]
}Returns the merchant's own choices for her permanent drops page, plus the two things the picker needs to offer real options rather than invented ones: her brand colour and the shop photo she has already uploaded.
Their permanent name in the link, once claimed.
The page this styles, at its own address: https://{handle}.membber.com/
plainbrandcolorimagelogochosenHer logo, or null.
fill = cover the circle; fit = the whole logo inside, on `disc`.
fillfitThe point of the logo at the circle centre, across (0..1).
The point of the logo at the circle centre, down (0..1).
1 = the short side spans the circle.
#RRGGBB behind the logo.
The logo file's width / height.
Her kind of shop: a Drops kind page slug ("cookies", "streetwear", "flowers", ...) or "something-else". Her own choice when she made one, else the kind page she signed up from. Null or absent: unknown, which reads as a food shop. Streetwear, flowers and something-else are not food: her catalogue is then called Items, not Menu.
curl -G "https://www.membber.com/api/v1/drops/shop" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/shop", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getDropsShop(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"shop": {
"handle": "<handle>",
"shop_url": "https://example.com/image.jpg",
"bg_mode": "plain",
"bg_value": "<bg_value>",
"brand_color": "<brand_color>",
"ribbon_color": "<ribbon_color>",
"ribbon_source": "logo",
"logo_url": "https://example.com/image.jpg",
"logo_display": {
"mode": "fill",
"x": 0,
"y": 0,
"scale": 1,
"disc": "<disc>",
"aspect": 1
},
"logo_swatches": [
"<logo_swatche>"
],
"photo_url": "https://example.com/image.jpg",
"description": "Added at the front desk",
"cta_label": "<cta_label>",
"cta_color": "<cta_color>",
"empty_title": "<empty_title>",
"empty_body": "<empty_body>",
"instagram": "<instagram>",
"facebook": "<facebook>",
"tiktok": "<tiktok>",
"sells": "<sells>"
}
}Sets the background of the shop's permanent drops page. `color` requires a #RRGGBB and `image` requires a URL; both are refused without one rather than silently falling back, because a picker that quietly ignores the choice is worse than one that says no. Also sets her ribbon colour (`ribbon_color`, which marks it as hers so a new logo never changes it) and how her logo sits in its circle (`logo_display`), and her social links (`instagram`, `facebook`, `tiktok`: a name or a link on that network's own domain, stored as one https link, refused otherwise), and what she sells (`sells`). Every field is optional; an absent one is left as it is.
plainbrandcolorimage#RRGGBB, or empty for the brand-derived colour.
Her Instagram name or link. Empty clears it.
Her Facebook page name or link. Empty clears it.
Her TikTok name or link. Empty clears it.
#RRGGBB for her ribbon (and her buttons, unless she chose a button colour). Marks it as hers.
How her logo sits in its circle. Refused when the shop has no logo.
fill = cover the circle; fit = the whole logo inside, on `disc`.
fillfitThe point of the logo at the circle centre, across (0..1).
The point of the logo at the circle centre, down (0..1).
1 = the short side spans the circle.
Her kind of shop, as she chose it in "You sell": a Drops kind page slug or "something-else". Anything else is refused (422).
Their permanent name in the link, once claimed.
The page this styles, at its own address: https://{handle}.membber.com/
plainbrandcolorimagelogochosenHer logo, or null.
fill = cover the circle; fit = the whole logo inside, on `disc`.
fillfitThe point of the logo at the circle centre, across (0..1).
The point of the logo at the circle centre, down (0..1).
1 = the short side spans the circle.
#RRGGBB behind the logo.
The logo file's width / height.
Her kind of shop: a Drops kind page slug ("cookies", "streetwear", "flowers", ...) or "something-else". Her own choice when she made one, else the kind page she signed up from. Null or absent: unknown, which reads as a food shop. Streetwear, flowers and something-else are not food: her catalogue is then called Items, not Menu.
curl -X PUT "https://www.membber.com/api/v1/drops/shop" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PUT("/api/v1/drops/shop", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.updateDropsShop(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"shop": {
"handle": "<handle>",
"shop_url": "https://example.com/image.jpg",
"bg_mode": "plain",
"bg_value": "<bg_value>",
"brand_color": "<brand_color>",
"ribbon_color": "<ribbon_color>",
"ribbon_source": "logo",
"logo_url": "https://example.com/image.jpg",
"logo_display": {
"mode": "fill",
"x": 0,
"y": 0,
"scale": 1,
"disc": "<disc>",
"aspect": 1
},
"logo_swatches": [
"<logo_swatche>"
],
"photo_url": "https://example.com/image.jpg",
"description": "Added at the front desk",
"cta_label": "<cta_label>",
"cta_color": "<cta_color>",
"empty_title": "<empty_title>",
"empty_body": "<empty_body>",
"instagram": "<instagram>",
"facebook": "<facebook>",
"tiktok": "<tiktok>",
"sells": "<sells>"
}
}/api/v1/drops/shop/logoClears the logo everywhere it is shown (drops pages, share icons, the App Clip) and deletes its files from storage unless something else still shows them. Idempotent: a shop with no logo gets the same answer.
Always null: the shop has no logo now.
curl -X DELETE "https://www.membber.com/api/v1/drops/shop/logo" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.DELETE("/api/v1/drops/shop/logo", {
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.removeShopLogo().ok.body.json
print(response){
"logo_url": "https://example.com/image.jpg"
}Reads the shop's loyalty on/off switch, the same column the Business app's Loyalty switch reads.
Whether the shop runs loyalty (stamps on orders, the stamp card).
On a write: false when the switch already said this and nothing was done. Always false on a read.
curl -G "https://www.membber.com/api/v1/drops/shop/loyalty" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/shop/loyalty", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getDropsShopLoyalty(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"loyalty_enabled": true,
"changed": true
}The same switch as the Business app's Loyalty switch, through the same server path. On: stamps start again and stamp cards come back to customers' Wallets. Off: stamps stop, rewards already earned stay valid until they expire, stamp cards are taken out of customers' Wallets, and the customers holding a reward are told. Setting the state it already has does nothing (`changed: false`). Needs a signed-in owner or a staff member allowed to change settings; an API key is refused.
true turns loyalty on, false turns it off.
Whether the shop runs loyalty (stamps on orders, the stamp card).
On a write: false when the switch already said this and nothing was done. Always false on a read.
curl -X PUT "https://www.membber.com/api/v1/drops/shop/loyalty" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"loyalty_enabled": true
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PUT("/api/v1/drops/shop/loyalty", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
loyalty_enabled: true
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.setDropsShopLoyalty(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
loyaltyEnabled: true
))
).ok.body.json
print(response){
"loyalty_enabled": true,
"changed": true
}Everything in the carousel, in the order the merchant put it. Pass `drop_id` for ONE drop's own pictures; omit it for the shop's own page. The two are separate carousels sharing one table.
The drop whose own photos these are. Omitted means the shop's own page.
imagevideoOne drop's pictures only (`drop_id` given): false when the merchant holds its header still for every buyer, true (the default) when it moves on by itself. Set with `header_auto_advance` on PATCH /api/v1/drops/{dropId}.
curl -G "https://www.membber.com/api/v1/drops/shop/media" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/shop/media");
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.listShopMedia().ok.body.json
print(response){
"media": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"url": "https://example.com/image.jpg",
"media_type": "image",
"title": "Morning class",
"thumbnail_url": "https://example.com/image.jpg",
"sort_order": 1,
"is_published": true
}
],
"auto_advance": true
}Records an ALREADY-UPLOADED file against the shop. The file itself goes up through the existing store-image uploader, so there is one uploader rather than two. New pieces are appended, never inserted at the front. Refused with CAROUSEL_FULL (409) once that page holds 10 pieces, the most it can hold: the shop's own page holds ten, and each drop ten of its own. The refusal's details name the page that is full: `place` is `shop` or `drop`. Up to THREE videos a page, the shop's own or one drop's (since 22 Oct 2026; one before): a video is refused with VIDEOS_FULL (409) when that page already holds three, whoever brought them in; the videos a page already holds are never removed by this rule. A drop's own photo (`image_url`) sent as one of its pieces goes in front of them.
imagevideoThe drop whose own photos these are. Omitted means the shop's own page.
imagevideoOne drop's pictures only (`drop_id` given): false when the merchant holds its header still for every buyer, true (the default) when it moves on by itself. Set with `header_auto_advance` on PATCH /api/v1/drops/{dropId}.
curl -X POST "https://www.membber.com/api/v1/drops/shop/media" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/image.jpg",
"media_type": "image"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/shop/media", {
body: {
url: "https://example.com/image.jpg",
media_type: "image"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.addShopMedia(
body: .json(.init(
url: "https://example.com/image.jpg",
mediaType: .image
))
).ok.body.json
print(response){
"media": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"url": "https://example.com/image.jpg",
"media_type": "image",
"title": "Morning class",
"thumbnail_url": "https://example.com/image.jpg",
"sort_order": 1,
"is_published": true
}
],
"auto_advance": true
}Scoped to the calling store, so one shop can never remove another's media. The file leaves storage too (with its sizes), unless something else still shows it.
The drop whose own photos these are. Omitted means the shop's own page.
imagevideoOne drop's pictures only (`drop_id` given): false when the merchant holds its header still for every buyer, true (the default) when it moves on by itself. Set with `header_auto_advance` on PATCH /api/v1/drops/{dropId}.
curl -X DELETE "https://www.membber.com/api/v1/drops/shop/media/3813101f-0000-4000-8000-d0c500000038?mediaId=3813101f-0000-4000-8000-d0c500000038" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.DELETE("/api/v1/drops/shop/media/{mediaId}", {
params: { path: { mediaId: "3813101f-0000-4000-8000-d0c500000038" }, query: { mediaId: "3813101f-0000-4000-8000-d0c500000038" } },
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.deleteShopMedia(
path: .init(mediaId: "3813101f-0000-4000-8000-d0c500000038"),
query: .init(mediaId: "3813101f-0000-4000-8000-d0c500000038")
).ok.body.json
print(response){
"media": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"url": "https://example.com/image.jpg",
"media_type": "image",
"title": "Morning class",
"thumbnail_url": "https://example.com/image.jpg",
"sort_order": 1,
"is_published": true
}
],
"auto_advance": true
}Keeps the piece's place in the running order. A film replaced by a photo becomes a photo. The old file leaves storage unless something else still shows it. Scoped to the calling store.
Supplied by the URL PATH.
The new photo, already uploaded through the store-image uploader.
The drop whose own photos these are. Omitted means the shop's own page.
imagevideoOne drop's pictures only (`drop_id` given): false when the merchant holds its header still for every buyer, true (the default) when it moves on by itself. Set with `header_auto_advance` on PATCH /api/v1/drops/{dropId}.
curl -X PATCH "https://www.membber.com/api/v1/drops/shop/media/3813101f-0000-4000-8000-d0c500000038" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/image.jpg"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PATCH("/api/v1/drops/shop/media/{mediaId}", {
params: { path: { mediaId: "3813101f-0000-4000-8000-d0c500000038" } },
body: {
url: "https://example.com/image.jpg"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.replaceShopMedia(
path: .init(mediaId: "3813101f-0000-4000-8000-d0c500000038"),
body: .json(.init(
url: "https://example.com/image.jpg"
))
).ok.body.json
print(response){
"media": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"url": "https://example.com/image.jpg",
"media_type": "image",
"title": "Morning class",
"thumbnail_url": "https://example.com/image.jpg",
"sort_order": 1,
"is_published": true
}
],
"auto_advance": true
}Every id is checked against this store before anything moves.
The drop whose own photos these are. Omitted means the shop's own page.
imagevideoOne drop's pictures only (`drop_id` given): false when the merchant holds its header still for every buyer, true (the default) when it moves on by itself. Set with `header_auto_advance` on PATCH /api/v1/drops/{dropId}.
curl -X PUT "https://www.membber.com/api/v1/drops/shop/media/order" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"ids": [
"00000d1b-0000-4000-8000-d0c500000000"
]
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PUT("/api/v1/drops/shop/media/order", {
body: {
ids: [
"00000d1b-0000-4000-8000-d0c500000000"
]
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.reorderShopMedia(
body: .json(.init(
ids: ["00000d1b-0000-4000-8000-d0c500000000"]
))
).ok.body.json
print(response){
"media": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"url": "https://example.com/image.jpg",
"media_type": "image",
"title": "Morning class",
"thumbnail_url": "https://example.com/image.jpg",
"sort_order": 1,
"is_published": true
}
],
"auto_advance": true
}Deletes one of the calling shop's own stored videos that no item, drop or shop page of hers shows, and answers with her list as it now is. A video that IS on an item, a drop or her shop page is removed there, through that place's own route, never here: this answers 409 VIDEO_IN_USE for one. 404 for a name that is not one of her own files, and while the video space rule is switched off.
The video's `name`, as the list gave it.
shop_filmsold_videoWhat her stored videos take, and any upload still on its way.
What she may store before more needs approval.
Each stored video, biggest first. Null when the list was not asked for.
The file as storage knows it (its name inside the shop video bucket). Hand it back to remove an unused one.
Its public address.
Its size as storage recorded it. Null when storage recorded none.
Every place of hers that shows it. Empty: nothing of hers uses it (uploaded and never added, or left on a removed item).
curl -X DELETE "https://www.membber.com/api/v1/drops/shop/videos?name=Example%20name" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.DELETE("/api/v1/drops/shop/videos", {
params: { query: { name: "Example name" } },
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.removeUnusedShopVideo(
query: .init(name: "Example name")
).ok.body.json
print(response){
"kind": "shop_film",
"used_bytes": 0,
"allowance_bytes": 0,
"videos": [
{
"name": "Example name",
"url": "https://example.com/image.jpg",
"bytes": 0,
"places": [
"<place>"
]
}
]
}The calling shop's own video space: used and allowance, in bytes, for its shop films (on items, among its shop pictures, on a drop, from Instagram) or, with `kind=sold_video`, the videos it sells as downloads. With `list=true` (shop films only) each stored video too, biggest first, with every place of hers that shows it; a video nothing of hers shows has no places. Only ever the calling shop's own files. 404 while the video space rule is switched off. 503 "Try again in a moment." when the count cannot be read: a number is never guessed.
Which allowance: her shop films (the default) or the videos she sells as downloads.
shop_filmsold_video`true` to also list each stored video. Shop films only; left out, only the two numbers are read.
truefalseshop_filmsold_videoWhat her stored videos take, and any upload still on its way.
What she may store before more needs approval.
Each stored video, biggest first. Null when the list was not asked for.
The file as storage knows it (its name inside the shop video bucket). Hand it back to remove an unused one.
Its public address.
Its size as storage recorded it. Null when storage recorded none.
Every place of hers that shows it. Empty: nothing of hers uses it (uploaded and never added, or left on a removed item).
curl -G "https://www.membber.com/api/v1/drops/shop/videos" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/drops/shop/videos");
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getShopVideos().ok.body.json
print(response){
"kind": "shop_film",
"used_bytes": 0,
"allowance_bytes": 0,
"videos": [
{
"name": "Example name",
"url": "https://example.com/image.jpg",
"bytes": 0,
"places": [
"<place>"
]
}
]
}Adds a phone number or email to a shop's drop list. PUBLIC, the person has no account and does not need one. A second submission re-subscribes rather than duplicating.
An email to be told on, instead of or as well as a mobile. Optional: leave it out, or send null or an empty string, for no email. Anything else must be exactly one address, or the request is refused.
phoneemailtextemailnonecurl -X POST "https://www.membber.com/api/v1/drops/subscribe/<handle>" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"handle": "<handle>",
"phone": "+44 7700 900123",
"email": "alex@example.com",
"consent_text": "<consent_text>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient();
const { data, error } = await membber.raw.POST("/api/v1/drops/subscribe/{handle}", {
params: { path: { handle: "<handle>" } },
body: {
handle: "<handle>",
phone: "+44 7700 900123",
email: "alex@example.com",
consent_text: "<consent_text>"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { nil }
)
let response = try await client.api.subscribeToDrops(
path: .init(handle: "<handle>"),
body: .json(.init(
handle: "<handle>",
phone: "+44 7700 900123",
email: "alex@example.com",
consentText: "<consent_text>"
))
).ok.body.json
print(response){
"ok": true,
"via": "phone",
"already": true,
"welcome": "text"
}