30 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 drops page: /{handle}/drops
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.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
Always null for an unlimited item.
null = no cap on orders in this window.
Sum of the counted items only; an unlimited item adds nothing.
Why a drop cannot open. Say this to the merchant, do not refuse blankly.
no_addoncollect_offno_paymentspayments_destinationunavailableVisits to this shop's public drops pages. Null only when the count could not be read.
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",
"price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1
}
],
"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>"
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"blockers": [
"<blocker>"
],
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
],
"live_drop_id": "68f0d418-0000-4000-8000-d0c500000068",
"drops_live": true,
"blocked_reason": "no_addon",
"reachable_customers": -9007199254740991,
"page_views": {
"shop_today": -9007199254740991,
"shop_last_7_days": -9007199254740991,
"drops_today": -9007199254740991,
"drops_last_7_days": -9007199254740991
}
}/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 drops page: /{handle}/drops
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.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
Always null for an unlimited item.
null = no cap on orders in this window.
Sum of the counted items only; an unlimited item adds nothing.
curl -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",
"price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1
}
],
"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>"
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"blockers": [
"<blocker>"
],
"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 drops page: /{handle}/drops
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.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
Always null for an unlimited item.
null = no cap on orders in this window.
Sum of the counted items only; an unlimited item adds nothing.
curl -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",
"price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1
}
],
"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>"
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"blockers": [
"<blocker>"
],
"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.
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.
The short code that identifies this drop.
The shop's permanent drops page: /{handle}/drops
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.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
Always null for an unlimited item.
null = no cap on orders in this window.
Sum of the counted items only; an unlimited item adds nothing.
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",
"price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1
}
],
"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>"
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"blockers": [
"<blocker>"
],
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
}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. Requires the refund permission.
The short code that identifies this drop.
The shop's permanent drops page: /{handle}/drops
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.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
Always null for an unlimited item.
null = no cap on orders in this window.
Sum of the counted items only; an unlimited item adds nothing.
How many orders will be refunded.
How many PEOPLE, which is fewer if anyone ordered twice.
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",
"price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1
}
],
"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>"
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"blockers": [
"<blocker>"
],
"created_at": "<created_at>",
"updated_at": "<updated_at>"
},
"orders_to_refund": 1,
"people_to_tell": 1,
"refund_pence": 1500
}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 drops page: /{handle}/drops
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.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
Always null for an unlimited item.
null = no cap on orders in this window.
Sum of the counted items only; an unlimited item adds nothing.
curl -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",
"price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1
}
],
"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>"
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"blockers": [
"<blocker>"
],
"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.
The short code that identifies this drop.
The shop's permanent drops page: /{handle}/drops
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.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
Always null for an unlimited item.
null = no cap on orders in this window.
Sum of the counted items only; an unlimited item adds nothing.
curl -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",
"price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1
}
],
"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>"
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"blockers": [
"<blocker>"
],
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
}Replaces the drop collection windows. A drop owns its own windows for one date rather than inheriting the store's standing weekly hours. Windows can be added while the drop is selling. Removing a window that people have already booked into is refused, and the refusal says how many.
The short code that identifies this drop.
The shop's permanent drops page: /{handle}/drops
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.
How many were put up. Meaningless when is_unlimited.
How many are still available. Never negative. Mirrors quantity when is_unlimited.
null = no limit.
Always null for an unlimited item.
null = no cap on orders in this window.
Sum of the counted items only; an unlimited item adds nothing.
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",
"price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"quantity": 1,
"remaining": 1,
"is_unlimited": true,
"sold": 1,
"max_per_order": 1,
"sold_out_at": "<sold_out_at>",
"sort_order": 1,
"allergens": [
"<allergen>"
],
"allergens_declared_none": true,
"allergens_missing": true
}
],
"windows": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"max_orders": 1,
"orders_remaining": 1
}
],
"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>"
}
],
"orders_count": 1,
"units_sold": 1,
"units_total": 1,
"has_unlimited": true,
"gross_pence": 1500,
"blockers": [
"<blocker>"
],
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
}Authorised by the PaymentIntent's client_secret, which only the browser that paid holds. Never minted from a remembered email: that is typed, never verified.
curl -X POST "https://www.membber.com/api/v1/drops/chat/claim" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"client_secret": "<client_secret>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/chat/claim", {
body: {
client_secret: "<client_secret>"
},
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.claimDropChat(
body: .json(.init(
clientSecret: "<client_secret>"
))
).ok.body.json
print(response){
"ok": true,
"chat_token": "<chat_token>",
"handle": "<handle>"
}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.
curl -X POST "https://www.membber.com/api/v1/drops/message/<handle>" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"email": "alex@example.com",
"message": "Added at the front desk"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
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"
},
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.startDropMessage(
path: .init(handle: "<handle>"),
body: .json(.init(
email: "alex@example.com",
message: "Added at the front desk"
))
).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" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "token=<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/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: { session.accessToken }
)
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>"
}
]
}Requires the chat key. Posts into the same conversation the merchant already sees.
Supplied by the URL PATH.
curl -X POST "https://www.membber.com/api/v1/drops/message/<handle>/chat" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-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({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
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: { session.accessToken }
)
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, 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 "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.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: { session.accessToken }
)
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.
curl -X POST "https://www.membber.com/api/v1/drops/message/<handle>/open" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"email": "alex@example.com"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/message/{handle}/open", {
params: { path: { handle: "<handle>" } },
body: {
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: { session.accessToken }
)
let response = try await client.api.openDropChat(
path: .init(handle: "<handle>"),
body: .json(.init(
email: "alex@example.com"
))
).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.
curl -X POST "https://www.membber.com/api/v1/drops/message/<handle>/verify" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-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({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
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: { session.accessToken }
)
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>"
}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 "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"client_secret": "<client_secret>",
"phone": "+44 7700 900123"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/drops/order-phone", {
body: {
client_secret: "<client_secret>",
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: { session.accessToken }
)
let response = try await client.api.addDropOrderPhone(
body: .json(.init(
clientSecret: "<client_secret>",
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.
destinationdirectcurl -X POST "https://www.membber.com/api/v1/drops/payment" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-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({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
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: { session.accessToken }
)
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>"
}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.
image | video
Still frame for a film.
scheduledliveclosedcollectedcancelledTheir permanent link: /{handle}/drops
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" \
-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/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: { session.accessToken }
)
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"
},
"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>"
}
],
"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,
"items": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"description": "Added at the front desk",
"price_pence": 1500,
"image_url": "https://example.com/image.jpg",
"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,
"ingredients": "<ingredients>"
}
],
"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",
"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 "Authorization: Bearer $MEMBBER_TOKEN" \
-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({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
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: { session.accessToken }
)
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 when someone empties their basket or leaves.
curl -X POST "https://www.membber.com/api/v1/drops/public/EXAMPLE10/release" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-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({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
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: { session.accessToken }
)
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.
phoneemailcurl -X POST "https://www.membber.com/api/v1/drops/remind/EXAMPLE10" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-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({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
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: { session.accessToken }
)
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 "Authorization: Bearer $MEMBBER_TOKEN" \
-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({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
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: { session.accessToken }
)
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>"
]
}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: /{handle}/drops
plainbrandcolorimagecurl -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>",
"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>"
}
}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.
plainbrandcolorimage#RRGGBB, or empty for the brand-derived colour.
Their permanent name in the link, once claimed.
The page this styles: /{handle}/drops
plainbrandcolorimagecurl -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",
"bg_mode": "plain"
}'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",
bg_mode: "plain"
},
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",
bgMode: .plain
))
).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>",
"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>"
}
}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.
imagevideocurl -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
}
]
}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.
imagevideoThe drop whose own photos these are. Omitted means the shop's own page.
imagevideocurl -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
}
]
}Scoped to the calling store, so one shop can never remove another's media.
The drop whose own photos these are. Omitted means the shop's own page.
imagevideocurl -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
}
]
}Every id is checked against this store before anything moves.
The drop whose own photos these are. Omitted means the shop's own page.
imagevideocurl -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
}
]
}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.
phoneemailtextemailnonecurl -X POST "https://www.membber.com/api/v1/drops/subscribe/<handle>" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-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({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
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: { session.accessToken }
)
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"
}