Skip to content

Integration Prompt

Copy-paste system prompt and code templates for wiring an AI agent to Telegram Shopping.

System prompt

Drop this into your agent's system prompt or context. This covers the full store lifecycle — from setup to daily management.

You manage a Telegram Shopping store through its REST API. The store is an LNbits extension that runs a Telegram bot and Mini App for Lightning-powered e-commerce.

## API Configuration
- Base URL: {LNBITS_URL}/telegramshop/api/v1
- Auth header: X-API-KEY: {WALLET_KEY}
- Admin key for writes, invoice key for reads
- All bodies and responses are JSON

## Information to Gather from the User

Before setting up a store, ask the user for:
1. LNbits URL — "What is your LNbits server URL?" (e.g. https://your-lnbits.com)
2. Wallet keys — "What is your LNbits wallet admin key?" (and invoice key)
3. Bot token — "What is your Telegram bot token from @BotFather?"
4. Currency — "What currency do you want to display prices in?" (sat, USD, EUR, etc.)
5. Checkout mode — "What information do you need from buyers at checkout?" (none = anonymous, email = email only, address = full shipping address)
6. Order tracking — "Do you want to track order fulfillment status?" (preparing → shipping → delivered)
7. Returns — "Do you want to allow customers to request returns?" If yes: "How many days should the return window be?" (default: 30)
8. Shipping rates — "Do you have shipping costs?" If yes: "What is your flat shipping rate? Is there a free shipping threshold? Any per-kg rate?"
9. Admin notifications — "Do you want order notifications in your personal Telegram chat? If so, what is your Telegram chat ID?"

## Store Setup (run once)

1. TEST TOKEN
   POST /shop/test-token
   Body: {"bot_token": "123:ABC"}
   Returns: {"username": "BotName", "first_name": "Bot Display Name"}
   → Validates the Telegram bot token. Fails with 400 if invalid.

2. FIND INVENTORY
   GET /sources/inventory
   Returns: [{"id": "inv_abc", "name": "My Inventory", "omit_tags": [...]}]
   → Gets the user's Inventory extension ID. Returns empty array if not set up.
   → If empty, tell the user: "You need to set up the Inventory extension in LNbits first and add some products."

3. CREATE SHOP
   POST /shop
   Body: {"title": "Shop Name", "bot_token": "123:ABC", "inventory_id": "inv_id", "currency": "USD", "checkout_mode": "email", "enable_order_tracking": true, "allow_returns": true, "return_window_hours": 720, "shipping_flat_rate": 5.0, "shipping_free_threshold": 50.0}
   Returns: shop object with id (bot_token excluded from response)

4. START BOT
   POST /shop/{id}/start
   Returns: {"success": true}
   → Enables bot, fetches products from Inventory, creates 4 default campaigns (all disabled).

5. VERIFY RUNNING
   GET /stats
   Returns: {..., "shops": 1, "shops_live": 1}
   → shops_live > 0 confirms the bot is active. This endpoint uses the wallet key — no shop_id needed.

6. CONFIGURE CAMPAIGNS
   GET /commercial?shop_id={id}
   → Lists 4 default campaigns (all disabled)
   PUT /commercial/{id}
   Body: {"content": "message text", "is_enabled": true, "delay_minutes": 30}
   → Enable and customize each campaign type

## Daily Operations

1. CHECK MESSAGES
   GET /message/unread-count?shop_id={id}
   Returns: {"count": 3}
   → If count > 0:
   GET /message?shop_id={id}&unread_only=true
   Returns: array of message objects (chat_id, content, direction, is_read, order_id)
   → direction "in" = customer message, "out" = admin message

2. REPLY TO CUSTOMER
   POST /message/{shop_id}
   Body: {"chat_id": 123, "content": "text", "order_id": "optional"}
   → Sends via Telegram bot and saves in message history

3. MARK MESSAGE READ
   PUT /message/{id}/read
   Returns: {"success": true}

4. VIEW CONVERSATION
   GET /message/thread?shop_id={id}&chat_id={chat_id}&order_id={optional}
   → Full conversation with a customer, optionally filtered to one order

5. LIST ORDERS
   GET /order?shop_id={id}&status=paid&limit=50&offset=0
   Returns: array of orders (id, amount_sats, currency_amount, cart_json, telegram_username, fulfillment_status, buyer_email, has_physical_items)
   → cart_json is a JSON string — parse it: [{"product_id": "...", "title": "...", "quantity": 1, "price": 10.0}]

6. UPDATE FULFILLMENT
   PUT /order/{id}/fulfillment
   Body: {"status": "shipping", "note": "Tracking: 1Z999..."}
   Status values: preparing → shipping → delivered
   → Only works on paid orders with order tracking enabled. Notifies customer via Telegram.

7. LIST RETURNS
   GET /return?shop_id={id}&status=requested&limit=50&offset=0
   Returns: array of return objects (id, order_id, items_json, reason, refund_amount_sats, status)

8. APPROVE RETURN
   PUT /return/{id}/approve
   Body: {"refund_method": "credit", "refund_amount_sats": 5000}
   → refund_method: "credit" (instant store credit) or "lightning" (Lightning refund)
   → refund_amount_sats: optional, defaults to full amount, cannot exceed original
   → IMPORTANT: Re-fetch the return before approving — status may have changed
   → Returns 400 if status is not "requested", 409 on race condition

9. DENY RETURN
   PUT /return/{id}/deny
   Body: {"admin_note": "reason for denial"}
   → Same status checks as approve

10. LIST SHOPS
    GET /shop
    Returns: array of shop objects (id, title, currency, checkout_mode, is_enabled, bot_username)

11. UPDATE SHOP
    PUT /shop/{id}
    Body: same as create — bot_token is preserved if omitted
    → Useful for changing settings like currency, checkout mode, shipping rates

12. REFRESH PRODUCTS
    POST /shop/{id}/refresh
    Returns: {"success": true}
    → Forces re-fetch from Inventory extension. Use after adding/changing products.

13. BROADCAST PROMOTION
    POST /commercial/{id}/broadcast
    Returns: {"success": true, "sent": 45, "total": 50}
    → Sends to all customers. Rate-limited (30/sec). Deduplicated.
    → Bot must be running (returns 400 if not)

14. VIEW CAMPAIGN LOGS
    GET /commercial/{id}/log?limit=50&offset=0
    Returns: array of send log entries (chat_id, sent_at, order_id)

15. SHOP STATISTICS
    GET /stats
    Returns: {total_orders, paid_orders, revenue_sats, orders_today, unread_messages, open_returns, customer_count, shops, shops_live}
    → Aggregates across all shops for this wallet. No shop_id param needed.

16. LIST CUSTOMERS
    GET /customer?shop_id={id}
    Returns: array of customer objects (chat_id, username, first_name, first_seen, last_active)

## Campaign Types
- abandoned_cart: auto-sends when cart is stale for delay_minutes (includes cart contents)
- back_in_stock: auto-sends when products go from 0 → >0 stock
- post_purchase: auto-sends after order is marked delivered
- promotion: manual broadcast only — never auto-triggered

## Rules
- Stats endpoint uses wallet key, NOT shop_id — it aggregates all shops
- Re-fetch return status before approve/deny — it may have changed since listing
- refund_amount_sats cannot exceed the return's original amount
- Always confirm with the user before: approving returns, denying returns, sending messages, broadcasting campaigns
- Order cart_json is a JSON string — parse it to see items
- buyer_address is also a JSON string when present — parse it for address fields
- Bot tokens are never returned in API responses — store them securely
- The bot and Mini App handle customer interactions — you manage the admin side
- Campaign broadcasts are deduplicated — safe to trigger multiple times
- Fulfillment updates only work on paid orders with order tracking enabled
- Use /sources/inventory to find the Inventory ID programmatically
- Use unread_only=true on message list to efficiently find new messages

Questions agents should ask humans

When helping a user integrate or manage Telegram Shopping, here are ready-made questions organized by context:

Initial setup

1. "What is your LNbits server URL?" (e.g. https://your-lnbits.com)
2. "What is your LNbits wallet admin key? You can find it in LNbits → your wallet → API info."
3. "Do you already have a Telegram bot? If not, message @BotFather on Telegram and use /newbot to create one. Then share the bot token with me."
4. "What currency do you want to display prices in? Common options: sat (satoshis), USD, EUR, GBP, CHF."
5. "What information should buyers provide at checkout?
   - None (anonymous purchases)
   - Email only
   - Full shipping address (for physical products)"

Shipping configuration

6. "Do you sell physical products that need shipping?"
7. "What is your flat shipping rate per order? (in your chosen currency)"
8. "Is there an order total above which shipping is free? If so, what amount?"
9. "Do you charge shipping by weight? If so, what is your per-kg rate?"

Returns and fulfillment

10. "Do you want to track order fulfillment? This lets you update customers with preparing → shipping → delivered status."
11. "Do you want to allow customers to request returns?"
12. "How many days after purchase should returns be accepted? (Default: 30 days)"
13. "For return refunds, do you want to offer store credit, Lightning refunds, or both?"

Daily operations

14. "Do you want me to check for new orders and messages? I'll summarize what needs attention."
15. "This customer is asking about their order. Here's their message: [content]. How should I reply?"
16. "There's a return request: [reason]. The refund would be [amount] sats. Should I approve (as credit or Lightning) or deny?"
17. "Order [id] is paid. Should I update the fulfillment status to 'preparing'?"

Campaigns

18. "Do you want to send abandoned cart reminders? I can enable this — customers with stale carts will get a nudge."
19. "Do you want back-in-stock notifications? When products return to inventory, all customers get alerted."
20. "Want to send a promotion to all customers? What should the message say?"

Python template

python
import os
import httpx

class TelegramShopClient:
    """AI-ready client for Telegram Shopping API."""

    def __init__(self):
        self.base = os.environ["LNBITS_URL"].rstrip("/")
        self.url = f"{self.base}/telegramshop/api/v1"
        self.admin_key = os.environ["LNBITS_ADMIN_KEY"]
        self.invoice_key = os.environ["LNBITS_INVOICE_KEY"]

    def _headers(self, write=False):
        key = self.admin_key if write else self.invoice_key
        return {"X-API-KEY": key, "Content-Type": "application/json"}

    # --- Setup ---

    async def test_token(self, bot_token):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.post(
                f"{self.url}/shop/test-token",
                json={"bot_token": bot_token},
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    async def inventory_sources(self):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(
                f"{self.url}/sources/inventory",
                headers=self._headers(),
            )
            r.raise_for_status()
            return r.json()

    async def create_shop(self, title, bot_token, inventory_id, **kwargs):
        body = {"title": title, "bot_token": bot_token,
                "inventory_id": inventory_id, **kwargs}
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.post(
                f"{self.url}/shop", json=body,
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    async def start_shop(self, shop_id):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.post(
                f"{self.url}/shop/{shop_id}/start",
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    async def stop_shop(self, shop_id):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.post(
                f"{self.url}/shop/{shop_id}/stop",
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    async def update_shop(self, shop_id, **kwargs):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.put(
                f"{self.url}/shop/{shop_id}",
                json=kwargs,
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    async def refresh_products(self, shop_id):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.post(
                f"{self.url}/shop/{shop_id}/refresh",
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    # --- Daily ops ---

    async def shops(self):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(f"{self.url}/shop", headers=self._headers())
            r.raise_for_status()
            return r.json()

    async def orders(self, shop_id, status="paid", limit=50, offset=0):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(
                f"{self.url}/order",
                params={"shop_id": shop_id, "status": status,
                        "limit": limit, "offset": offset},
                headers=self._headers(),
            )
            r.raise_for_status()
            return r.json()

    async def get_order(self, order_id):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(
                f"{self.url}/order/{order_id}",
                headers=self._headers(),
            )
            r.raise_for_status()
            return r.json()

    async def update_fulfillment(self, order_id, status, note=None):
        body = {"status": status}
        if note:
            body["note"] = note
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.put(
                f"{self.url}/order/{order_id}/fulfillment",
                json=body,
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    async def unread_count(self, shop_id):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(
                f"{self.url}/message/unread-count",
                params={"shop_id": shop_id},
                headers=self._headers(),
            )
            r.raise_for_status()
            return r.json()

    async def messages(self, shop_id, unread_only=False, limit=50, offset=0):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(
                f"{self.url}/message",
                params={"shop_id": shop_id, "unread_only": unread_only,
                        "limit": limit, "offset": offset},
                headers=self._headers(),
            )
            r.raise_for_status()
            return r.json()

    async def thread(self, shop_id, chat_id, order_id=None):
        params = {"shop_id": shop_id, "chat_id": chat_id}
        if order_id:
            params["order_id"] = order_id
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(
                f"{self.url}/message/thread",
                params=params,
                headers=self._headers(),
            )
            r.raise_for_status()
            return r.json()

    async def send_message(self, shop_id, chat_id, content, order_id=None):
        body = {"chat_id": chat_id, "content": content}
        if order_id:
            body["order_id"] = order_id
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.post(
                f"{self.url}/message/{shop_id}",
                json=body,
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    async def mark_read(self, message_id):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.put(
                f"{self.url}/message/{message_id}/read",
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    async def returns(self, shop_id, status="requested", limit=50, offset=0):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(
                f"{self.url}/return",
                params={"shop_id": shop_id, "status": status,
                        "limit": limit, "offset": offset},
                headers=self._headers(),
            )
            r.raise_for_status()
            return r.json()

    async def get_return(self, return_id):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(
                f"{self.url}/return/{return_id}",
                headers=self._headers(),
            )
            r.raise_for_status()
            return r.json()

    async def approve_return(self, return_id, method="credit", amount=None):
        body = {"refund_method": method}
        if amount is not None:
            body["refund_amount_sats"] = amount
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.put(
                f"{self.url}/return/{return_id}/approve",
                json=body,
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    async def deny_return(self, return_id, note):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.put(
                f"{self.url}/return/{return_id}/deny",
                json={"admin_note": note},
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    # --- Campaigns ---

    async def campaigns(self, shop_id):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(
                f"{self.url}/commercial",
                params={"shop_id": shop_id},
                headers=self._headers(),
            )
            r.raise_for_status()
            return r.json()

    async def create_campaign(self, shop_id, type, title, content, **kwargs):
        body = {"shop_id": shop_id, "type": type,
                "title": title, "content": content, **kwargs}
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.post(
                f"{self.url}/commercial",
                json=body,
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    async def update_campaign(self, commercial_id, **kwargs):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.put(
                f"{self.url}/commercial/{commercial_id}",
                json=kwargs,
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    async def delete_campaign(self, commercial_id):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.delete(
                f"{self.url}/commercial/{commercial_id}",
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    async def broadcast(self, commercial_id):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.post(
                f"{self.url}/commercial/{commercial_id}/broadcast",
                headers=self._headers(write=True),
            )
            r.raise_for_status()
            return r.json()

    async def campaign_logs(self, commercial_id, limit=50, offset=0):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(
                f"{self.url}/commercial/{commercial_id}/log",
                params={"limit": limit, "offset": offset},
                headers=self._headers(),
            )
            r.raise_for_status()
            return r.json()

    # --- Analytics ---

    async def stats(self):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(
                f"{self.url}/stats",
                headers=self._headers(),
            )
            r.raise_for_status()
            return r.json()

    async def customers(self, shop_id):
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(
                f"{self.url}/customer",
                params={"shop_id": shop_id},
                headers=self._headers(),
            )
            r.raise_for_status()
            return r.json()

Set environment variables:

bash
export LNBITS_URL="https://your-lnbits.com"
export LNBITS_ADMIN_KEY="your_admin_key"
export LNBITS_INVOICE_KEY="your_invoice_key"

cURL one-liners

bash
# Test a bot token
curl -s -X POST -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \
  -d '{"bot_token":"123:ABC"}' $LNBITS_URL/telegramshop/api/v1/shop/test-token

# Find Inventory ID
curl -s -H "X-API-KEY: $KEY" $LNBITS_URL/telegramshop/api/v1/sources/inventory

# Create a shop
curl -s -X POST -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \
  -d '{"title":"My Shop","bot_token":"123:ABC","inventory_id":"inv_abc","currency":"USD"}' \
  $LNBITS_URL/telegramshop/api/v1/shop

# List shops
curl -s -H "X-API-KEY: $KEY" $LNBITS_URL/telegramshop/api/v1/shop

# Start a shop
curl -s -X POST -H "X-API-KEY: $KEY" $LNBITS_URL/telegramshop/api/v1/shop/$SHOP/start

# Stop a shop
curl -s -X POST -H "X-API-KEY: $KEY" $LNBITS_URL/telegramshop/api/v1/shop/$SHOP/stop

# Refresh products from Inventory
curl -s -X POST -H "X-API-KEY: $KEY" $LNBITS_URL/telegramshop/api/v1/shop/$SHOP/refresh

# Shop statistics (no shop_id needed — uses wallet key)
curl -s -H "X-API-KEY: $KEY" $LNBITS_URL/telegramshop/api/v1/stats

# Recent paid orders
curl -s -H "X-API-KEY: $KEY" \
  "$LNBITS_URL/telegramshop/api/v1/order?shop_id=$SHOP&status=paid"

# Update fulfillment
curl -s -X PUT -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \
  -d '{"status":"shipping","note":"Tracking: 1Z999..."}' \
  $LNBITS_URL/telegramshop/api/v1/order/$ORDER/fulfillment

# Unread messages
curl -s -H "X-API-KEY: $KEY" \
  "$LNBITS_URL/telegramshop/api/v1/message/unread-count?shop_id=$SHOP"

# List unread messages only
curl -s -H "X-API-KEY: $KEY" \
  "$LNBITS_URL/telegramshop/api/v1/message?shop_id=$SHOP&unread_only=true"

# Get conversation thread (optionally filter by order)
curl -s -H "X-API-KEY: $KEY" \
  "$LNBITS_URL/telegramshop/api/v1/message/thread?shop_id=$SHOP&chat_id=$CHAT_ID"

# Send a message
curl -s -X POST -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \
  -d '{"chat_id":123456789,"content":"Your order shipped!"}' \
  $LNBITS_URL/telegramshop/api/v1/message/$SHOP

# Mark message as read
curl -s -X PUT -H "X-API-KEY: $KEY" \
  $LNBITS_URL/telegramshop/api/v1/message/$MSG_ID/read

# List return requests
curl -s -H "X-API-KEY: $KEY" \
  "$LNBITS_URL/telegramshop/api/v1/return?shop_id=$SHOP&status=requested"

# Approve a return with store credit
curl -s -X PUT -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \
  -d '{"refund_method":"credit"}' \
  $LNBITS_URL/telegramshop/api/v1/return/$RETURN_ID/approve

# Approve with partial Lightning refund
curl -s -X PUT -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \
  -d '{"refund_method":"lightning","refund_amount_sats":5000}' \
  $LNBITS_URL/telegramshop/api/v1/return/$RETURN_ID/approve

# Deny a return
curl -s -X PUT -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \
  -d '{"admin_note":"Item was used, not eligible for return."}' \
  $LNBITS_URL/telegramshop/api/v1/return/$RETURN_ID/deny

# List campaigns
curl -s -H "X-API-KEY: $KEY" \
  "$LNBITS_URL/telegramshop/api/v1/commercial?shop_id=$SHOP"

# Enable a campaign
curl -s -X PUT -H "X-API-KEY: $KEY" -H "Content-Type: application/json" \
  -d '{"is_enabled":true,"content":"Check out our latest products!"}' \
  $LNBITS_URL/telegramshop/api/v1/commercial/$CAMPAIGN_ID

# Broadcast a promotion
curl -s -X POST -H "X-API-KEY: $KEY" \
  $LNBITS_URL/telegramshop/api/v1/commercial/$CAMPAIGN_ID/broadcast

# View campaign send log
curl -s -H "X-API-KEY: $KEY" \
  "$LNBITS_URL/telegramshop/api/v1/commercial/$CAMPAIGN_ID/log?limit=20"

# List customers
curl -s -H "X-API-KEY: $KEY" \
  "$LNBITS_URL/telegramshop/api/v1/customer?shop_id=$SHOP"

Validation checklist

  • [ ] Can validate a bot token (POST /shop/test-token)
  • [ ] Can fetch inventory sources (GET /sources/inventory)
  • [ ] Can create and start a shop
  • [ ] Can list shops with invoice key
  • [ ] Can list orders filtered by shop and status
  • [ ] Can get a single order by ID
  • [ ] Can update fulfillment status with admin key
  • [ ] Can check unread message count
  • [ ] Can list messages with unread_only=true
  • [ ] Can view a conversation thread
  • [ ] Can send a message to a customer
  • [ ] Can mark a message as read
  • [ ] Can list returns filtered by status
  • [ ] Can approve a return (check for 400/409 on duplicate)
  • [ ] Can deny a return with a note
  • [ ] Can list and update marketing campaigns
  • [ ] Can broadcast a promotion (check sent vs total)
  • [ ] Can view campaign send logs
  • [ ] Can fetch shop statistics (no shop_id param)
  • [ ] Can list customers
  • [ ] Can refresh products from Inventory
  • [ ] Read operations fail gracefully with wrong key