Skip to content

Waillio/BotMarketingChatBotBackend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Gurugrow Auth Backend

Production-minded backend for one-time messenger account authorization from BotMarketing to RetailCRM.

Stack

  • Python 3.12
  • FastAPI
  • httpx
  • pydantic
  • SQLAlchemy + SQLite
  • uvicorn
  • python-dotenv
  • standard logging

Project Structure

app/
  main.py
  config.py
  schemas.py
  models.py
  db.py
  routers/auth.py
  services/retailcrm.py
  services/deduper.py
  services/greensms.py
  services/recommended_offer_catalog.py
  services/recommended_orders.py
  services/otp.py
  services/messenger_identity.py
  services/phone.py
  utils/masking.py
  utils/time.py

Endpoints

  • GET /health
  • GET /auth/status
  • POST /auth/start
  • POST /auth/check
  • POST /auth/resend
  • POST /auth/unlink
  • GET /orders
  • POST /orders/repeat
  • POST /orders/recommended/preview
  • POST /orders/recommended/create
  • POST /orders/recommended/start
  • POST /orders/recommended/check

POST endpoints accept both application/json and application/x-www-form-urlencoded.

Configuration

Copy .env.example to .env and fill real values:

cp .env.example .env

Important values:

  • RETAILCRM_BASE_URL: RetailCRM account base URL, for example https://example.retailcrm.ru
  • RETAILCRM_API_KEY: RetailCRM API key
  • RETAILCRM_SITE: RetailCRM site code if your account requires it
  • RETAILCRM_CF_TELEGRAM_ID: custom field code for Telegram stable account id
  • RETAILCRM_CF_VK_ID: custom field code for VK stable account id
  • RETAILCRM_CF_WHATSAPP_ID: custom field code for WhatsApp stable account id
  • RETAILCRM_CF_MAX_ID: custom field code for MAX stable account id
  • RETAILCRM_CF_INSTAGRAM_ID: custom field code for Instagram stable account id
  • RETAILCRM_CF_LAST_VERIFIED_CHANNEL: service custom field for the last verified channel
  • RETAILCRM_CF_LAST_VERIFIED_AT: service custom field for verification datetime
  • RETAILCRM_CF_LAST_VERIFICATION_METHOD: service custom field for the actual OTP method
  • GREENSMS_TOKEN: GreenSMS JWT/bearer token, if you use token auth
  • GREENSMS_USER and GREENSMS_PASS: GreenSMS username/password, if you use user/pass auth
  • GREENSMS_SMS_FROM: approved SMS sender name
  • GREENSMS_VK_FROM: approved VK sender shortname, required by GreenSMS /vk/send
  • DEDUPE_BASE_URL: deduper API base URL, for example https://dedupe.example.com
  • DEDUPE_API_KEY: API key sent as X-API-Key to the deduper
  • DEDUPE_TIMEOUT_SECONDS: bounded timeout for deduper calls, default 10
  • RETAILCRM_ORDER_EXCLUDED_STATUS_CODES: comma-separated order status codes hidden from /orders
  • RETAILCRM_ORDER_EXCLUDED_STATUS_NAMES: comma-separated order status names hidden from /orders
  • RETAILCRM_ORDER_EXCLUDED_GROUP_CODES: comma-separated RetailCRM status group codes hidden from /orders
  • RETAILCRM_ORDER_GROUP_COMPLETE_CODES: status group codes rendered as complete
  • RETAILCRM_ORDER_GROUP_DELIVERY_CODES: status group codes rendered as delivery
  • RETAILCRM_ORDER_GROUP_COMPLETE_NAMES: status group names rendered as complete
  • RETAILCRM_ORDER_GROUP_DELIVERY_NAMES: status group names rendered as delivery
  • ORDERS_DEFAULT_LIMIT: default number of orders shown by /orders, max 5
  • ORDERS_MAX_LOOKBACK: how many recent CRM orders to fetch before local filtering; RetailCRM supports page sizes 20, 50, 100
  • REPEAT_ORDER_NUMBER_PREFIX: prefix for repeated order numbers, default CHT
  • REPEAT_ORDER_NUMBER_SUFFIX: suffix for repeated order numbers, default -BOT
  • REPEAT_ORDER_COMMENT_TEMPLATE: manager-facing comment template for repeated orders

Repeated order numbers are generated as CHT<short random value>-BOT by default. The original RetailCRM order number is kept in sourceOrderNumber and manager comments, but is not embedded into the new order number.

GreenSMS official docs are at https://api3.greensms.ru/. The implemented calls use documented endpoints:

  • POST /telegram/send: to, txt, optional ttl
  • POST /vk/send: to, txt, from
  • POST /sms/send: to, txt, optional from
  • Auth: Authorization: Bearer <token> or user / pass

Messenger Identity and CRM Mapping

Stable messenger identity is (channel.type, customer.external_id).

The backend extracts the messenger account id in this order:

  • messengerUserId
  • customerExternalId
  • messengerAccountId
  • messengerAccountIdFallback

messengerAccountIdFallback is intended for BotMarketing's last_message.from.external_id fallback. chatExternalId is only a service/chat identifier and is never used as the CRM binding key. Username, first name and display name are not accepted for authorization.

Final channel mapping:

channelType RetailCRM field env Default field code
telegram RETAILCRM_CF_TELEGRAM_ID bot_telegram_account_id
vk RETAILCRM_CF_VK_ID bot_vk_account_id
whatsapp RETAILCRM_CF_WHATSAPP_ID bot_whatsapp_account_id
max RETAILCRM_CF_MAX_ID bot_max_account_id
instagram RETAILCRM_CF_INSTAGRAM_ID bot_instagram_account_id

Service fields written after successful /auth/check:

Purpose RetailCRM field env Default field code
Last verified channel RETAILCRM_CF_LAST_VERIFIED_CHANNEL bot_last_verified_channel
Last verified datetime RETAILCRM_CF_LAST_VERIFIED_AT bot_last_verified_at
Actual verification method RETAILCRM_CF_LAST_VERIFICATION_METHOD bot_last_verification_method

bot_last_verification_method stores the actual route that accepted the code delivery request: telegram_otp, vk_otp, or sms_otp. If Telegram/VK falls back to SMS, the stored value is sms_otp.

The backend also marks customer chatbot interaction in RetailCRM best-effort by setting customer custom field chatbot_interacted=true. This is a customer field, not an order field. Marking is attempted when a customer can be safely resolved by explicit customerId, current messenger binding, successful auth/check, orders/repeat flows, or recommended order flows. Marking errors are logged and do not fail the main BotMarketing scenario.

Phone Dedupe In /auth/start

/auth/start keeps the existing behavior for clear phone lookup results:

  • 0 RetailCRM customers: creates the current anti-enumeration fake OTP session and does not send a code.
  • 1 RetailCRM customer: creates a normal OTP session for that customer.
  • More than 1 RetailCRM customer: calls POST {DEDUPE_BASE_URL}/api/dedupe/auto-resolve.

Deduper request:

{
  "phone": "79991234567",
  "site": "your-site",
  "requestSource": "auth-backend",
  "requestId": "uuid4",
  "waitForConfirmation": false
}

The request is sent with X-API-Key: {DEDUPE_API_KEY} and uses DEDUPE_TIMEOUT_SECONDS.

Usable deduper statuses are single, resolved, and resolved_with_fallback_primary. For these statuses, finalCustomerId is saved into the OTP session as crm_customer_id. /auth/check then uses this session value and does not search by phone again.

If the deduper returns not_found, error, an empty finalCustomerId, times out, or is unavailable, /auth/start returns status="error" with a neutral message and does not send an OTP.

Run Locally

python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
cp .env.example .env
uvicorn app.main:app --host 0.0.0.0 --port 8000

SQLite tables are created automatically on startup.

Docker

cp .env.example .env
docker compose up -d --build

The SQLite database is persisted in ./data.

systemd Deployment Note

Example unit:

[Unit]
Description=Gurugrow Auth Backend
After=network.target

[Service]
WorkingDirectory=/opt/gurugrow-auth
EnvironmentFile=/opt/gurugrow-auth/.env
ExecStart=/opt/gurugrow-auth/.venv/bin/uvicorn app.main:app --host ${APP_HOST} --port ${APP_PORT}
Restart=always
RestartSec=5
User=www-data

[Install]
WantedBy=multi-user.target

BotMarketing Connection

Replace YOUR-AUTH-SERVER.example.com in BotMarketing with your backend host:

  • https://your-host.example.com/auth/status
  • https://your-host.example.com/auth/start
  • https://your-host.example.com/auth/check
  • https://your-host.example.com/auth/resend

Use application/x-www-form-urlencoded for POST requests. Returned fields are stable:

  • authStatusResponse.status
  • authStatusResponse.customerId
  • authStatusResponse.maskedPhone
  • authStatusResponse.errorCode
  • authStatusResponse.message
  • authStartResponse.status
  • authStartResponse.sessionId
  • authStartResponse.maskedPhone
  • authStartResponse.retryAfterSec
  • authStartResponse.errorCode
  • authStartResponse.message
  • authCheckResponse.status
  • authCheckResponse.authorized
  • authCheckResponse.customerId
  • authCheckResponse.verifiedUntil
  • authCheckResponse.maskedPhone
  • authCheckResponse.errorCode
  • authCheckResponse.message
  • authResendResponse.status
  • authResendResponse.retryAfterSec
  • authResendResponse.maskedPhone
  • authResendResponse.errorCode
  • authResendResponse.message

Accepted identity input fields are tolerant for BotMarketing scenarios: messengerUserId, customerExternalId, messengerAccountId, and messengerAccountIdFallback.

For the "My orders" block, configure BotMarketing to call:

  • http://your-host:8000/orders
  • http://your-host:8000/orders/repeat

Required query params:

  • channelType
  • one stable identity value: preferably messengerUserId or customerExternalId

Optional query params:

  • chatExternalId
  • customerId
  • limit, 1..5

Important: /orders does not trust customerId by itself. It first resolves the customer through the current messenger binding, then uses customerId only as a consistency check. If the supplied customerId differs from the bound CRM customer, the endpoint returns status="conflict" and does not return orders.

/orders/repeat uses the same rule. It first resolves the bound customer through messenger identity, validates optional customerId, loads sourceOrderId or fallback sourceOrderNumber, and checks that the source order belongs to the bound customer before creating anything.

/auth/unlink also uses the current messenger binding as the access key. It finds the CRM customer by channelType + messenger account id; optional customerId is only a consistency check. On success it clears only the channel binding field, for example bot_telegram_account_id. It does not clear bot_last_verified_channel, bot_last_verified_at, or bot_last_verification_method.

Recommended Orders

BotMarketing can create orders for recommended sets through:

  • POST /orders/recommended/preview
  • POST /orders/recommended/create
  • POST /orders/recommended/start
  • POST /orders/recommended/check

The frontend sends only offerCode. The mapping offerCode -> bundleArticle/title/components lives in app/services/recommended_offer_catalog.py. components are used only for preview text. Before order creation, the backend resolves bundleArticle through RetailCRM /api/v5/store/products; the order receives a real SKU reference through offer.id, offer.externalId, or offer.xmlId with quantity=1, so RetailCRM recalculates current prices from the catalog. If the bundle article is not found in RetailCRM, the order is not created as a free noname item.

Recommended orders are created with:

  • site: 1minoxidil-ru
  • status: ruchnaia-obrabotka
  • no manager assignment
  • no delivery/address
  • customerComment: Заказ создан чат-ботом по рекомендации: {offerCode}

For an authorized customer, BotMarketing calls /orders/recommended/preview, then /orders/recommended/create. The backend resolves the current customer through messenger binding and treats customerId only as a consistency check.

For a non-authorized customer, BotMarketing calls /orders/recommended/start, then /orders/recommended/check. start sends an OTP and stores offerCode, recipient data, normalized phone, optional resolved CRM customer id, and a flag to create a customer after successful OTP. check verifies the OTP, creates a CRM customer if needed, binds the messenger account, and creates the order.

Invalid email values are ignored and treated as empty. If RetailCRM order creation fails with a transient error, the backend does one retry. If the retry also fails, the response contains manager handoff text.

cURL Examples

Health:

curl "http://localhost:8000/health"

Status:

curl "http://localhost:8000/auth/status?channelType=telegram&chatExternalId=chat-1&customerExternalId=cust-1&messengerUserId=123456789"

Start, urlencoded:

curl -X POST "http://localhost:8000/auth/start" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "phone=+7 999 123-45-67" \
  -d "channelType=telegram" \
  -d "deliveryRoute=telegram" \
  -d "chatExternalId=chat-1" \
  -d "customerExternalId=cust-1" \
  -d "messengerUserId=123456789"

Start, JSON:

curl -X POST "http://localhost:8000/auth/start" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+7 999 123-45-67","channelType":"telegram","deliveryRoute":"telegram","chatExternalId":"chat-1","customerExternalId":"cust-1","messengerUserId":"123456789"}'

If RetailCRM has duplicates for that phone, the same /auth/start request goes through the deduper automatically before the OTP is sent:

curl -X POST "http://localhost:8000/auth/start" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "phone=+7 999 123-45-67" \
  -d "channelType=telegram" \
  -d "deliveryRoute=telegram" \
  -d "chatExternalId=chat-1" \
  -d "customerExternalId=123456789" \
  -d "messengerUserId=123456789"

Check, urlencoded:

curl -X POST "http://localhost:8000/auth/check" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "sessionId=SESSION_ID" \
  -d "code=123456" \
  -d "channelType=telegram" \
  -d "chatExternalId=chat-1" \
  -d "messengerUserId=123456789"

Resend, urlencoded:

curl -X POST "http://localhost:8000/auth/resend" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "sessionId=SESSION_ID" \
  -d "channelType=telegram" \
  -d "deliveryRoute=telegram"

Unlink, urlencoded:

curl -X POST "http://localhost:8000/auth/unlink" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "channelType=telegram" \
  -d "customerExternalId=73110987" \
  -d "messengerUserId=73110987" \
  -d "customerId=626518"

Orders:

curl "http://localhost:8000/orders?channelType=telegram&customerExternalId=123456789&messengerUserId=123456789&chatExternalId=chat-1&customerId=626518"

Repeat order, urlencoded:

curl -X POST "http://localhost:8000/orders/repeat" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "channelType=telegram" \
  -d "customerExternalId=123456789" \
  -d "messengerUserId=123456789" \
  -d "customerId=626518" \
  -d "sourceOrderId=12345"

Repeat order, JSON:

curl -X POST "http://localhost:8000/orders/repeat" \
  -H "Content-Type: application/json" \
  -d '{"channelType":"telegram","customerExternalId":"123456789","messengerUserId":"123456789","customerId":"626518","sourceOrderId":"12345"}'

Recommended preview:

curl -X POST "http://localhost:8000/orders/recommended/preview" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "offerCode=male_hairloss_basic" \
  -d "channelType=telegram" \
  -d "customerExternalId=123456789" \
  -d "messengerUserId=123456789" \
  -d "customerId=626518"

Recommended create for authorized customer:

curl -X POST "http://localhost:8000/orders/recommended/create" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "offerCode=male_hairloss_basic" \
  -d "customerId=626518" \
  -d "firstName=Иван" \
  -d "lastName=Иванов" \
  -d "phone=79991234567" \
  -d "email=mail@example.com" \
  -d "channelType=telegram" \
  -d "customerExternalId=123456789" \
  -d "messengerUserId=123456789"

Recommended start for non-authorized customer:

curl -X POST "http://localhost:8000/orders/recommended/start" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "offerCode=male_hairloss_basic" \
  -d "firstName=Иван" \
  -d "lastName=Иванов" \
  -d "phone=79991234567" \
  -d "email=mail@example.com" \
  -d "channelType=telegram" \
  -d "customerExternalId=123456789" \
  -d "messengerUserId=123456789" \
  -d "deliveryRoute=telegram"

Recommended check:

curl -X POST "http://localhost:8000/orders/recommended/check" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "sessionId=SESSION_ID" \
  -d "code=123456" \
  -d "channelType=telegram" \
  -d "customerExternalId=123456789" \
  -d "messengerUserId=123456789"

Successful /orders response:

{
  "status": "ok",
  "customerId": "626518",
  "shownCount": 1,
  "orders": [
    {
      "sourceOrderId": "12345",
      "sourceOrderNumber": "12345",
      "orderNumber": "12345",
      "createdAt": "2026-06-01T10:00:00Z",
      "createdAtText": "01.06.2026",
      "itemsText": "• Миноксидил 5% x1",
      "deliveryAddressText": "Москва, Тверская",
      "statusGroup": "delivery",
      "statusLabel": "В доставке",
      "deliveryCode": "pr-rs",
      "trackingCode": "TRACK123456",
      "trackingUrl": "https://www.pochta.ru/tracking?barcode=TRACK123456",
      "canTrackOnline": true,
      "detailText": "Вот этот заказ 👇\n\n12345\n01.06.2026\nВ доставке\n\n• Миноксидил 5% x1\n\nПочта РФ: Москва, Тверская\n\nЧто сделаем?",
      "repeatConfirmText": "Заказ 12345\n\nВот этот заказ 👇\n\n12345\n01.06.2026\n\n🗒️Состав:\n• Миноксидил 5% x1\n\n🚙Доставка:\nМосква, Тверская\n\nУверены, что хотите его повторить?",
      "trackingText": "📬 Ваш заказ уже едет к вам\n\nВот ссылка для отслеживания 👇\nhttps://www.pochta.ru/tracking?barcode=TRACK123456",
      "trackingNumber": "TRACK123456",
      "deliveryStatus": "В доставке"
    }
  ],
  "text": "Твои заказы 👇\n\nМожно отследить текущий заказ или быстро повторить прошлый.\n\n🗒️Номер заказа: 12345\n📆Оформлен: 01.06.2026\n\n🚚 Уже едет к вам\n\n• Миноксидил 5% x1\n\n🚙Москва, Тверская\n🔍Отследить заказ: https://www.pochta.ru/tracking?barcode=TRACK123456",
  "errorCode": null,
  "message": null
}

Each order contains:

  • orderNumber
  • sourceOrderId
  • sourceOrderNumber
  • createdAt
  • createdAtText
  • itemsText
  • deliveryAddressText
  • statusGroup: assembling, delivery, or complete
  • statusLabel
  • deliveryCode
  • trackingCode
  • trackingUrl
  • canTrackOnline
  • detailText
  • repeatConfirmText: clean confirmation text for BotMarketing repeat-order confirmation; unlike detailText, it does not include status or the "Что сделаем?" prompt
  • trackingText
  • compatibility aliases: trackingNumber, deliveryStatus

Tracking URLs are built from deliveryCode and trackingCode:

  • 5post: https://fivepost.ru/tracking/?id={trackingCode}
  • pr-rs: https://www.pochta.ru/tracking?barcode={trackingCode}
  • sdek-v-2-7b30522f5b0a597f679345a92b57c324: https://www.cdek.ru/ru/tracking/?order_id={trackingCode}

For other delivery codes, or when there is no tracking code, trackingUrl=null and canTrackOnline=false.

Status groups are resolved through RetailCRM /api/v5/reference/statuses:

  • complete: group code/name configured by RETAILCRM_ORDER_GROUP_COMPLETE_*
  • delivery: group code/name configured by RETAILCRM_ORDER_GROUP_DELIVERY_*
  • assembling: every other non-excluded status group

/orders statuses:

  • ok: orders found and returned
  • empty: authorized customer has no visible non-excluded orders in the lookback window
  • not_authorized: messenger account is not bound to a CRM customer
  • conflict: ambiguous binding or customerId mismatch
  • error: CRM or request error

/auth/unlink response on success:

{
  "status": "unlinked",
  "customerId": "626518",
  "channelType": "telegram",
  "text": "Учётная запись отвязана. Для входа в защищённые разделы потребуется снова подтвердить номер телефона.",
  "errorCode": null,
  "message": null
}

/auth/unlink statuses:

  • unlinked: current messenger account binding was cleared
  • not_authorized: current messenger account is already not bound
  • conflict: ambiguous binding or customerId mismatch
  • error: unsupported channel, invalid identity, or RetailCRM error

/orders/repeat response on success:

{
  "status": "created",
  "customerId": "626518",
  "sourceOrderId": "123456",
  "sourceOrderNumber": "ELM278345",
  "newOrderId": "987654",
  "newOrderNumber": "CHT48291357-BOT",
  "unavailableItemName": null,
  "text": "Заказ оформлен! 👍\n\nНомер вашего заказа: CHT48291357-BOT\n\nПока что мы не можем оформить оплату через бота, поэтому придётся подключить менеджера. Уже спешим на помощь! 🦸\n\nА если хотите что-то поменять, то можете пока написать, что нужно скорректировать, мы обязательно поможем.",
  "errorCode": null,
  "message": null
}

Repeated order creation:

  • Copies items and quantities, delivery address, phone, recipient name, and email.
  • Does not copy historical prices, old manager, or old status.
  • Creates the new order in status new.
  • Sends only offer id/externalId/xmlId and quantity for items, so RetailCRM can recalculate current prices.
  • Builds number as CHT<short random value>-BOT.
  • If RetailCRM rejects the number as duplicate, retries with a newly generated random value.
  • Adds manager-facing customerComment from REPEAT_ORDER_COMMENT_TEMPLATE.

If RetailCRM rejects an item because it is missing/unavailable, /orders/repeat returns status="unavailable_item" and does not create a partial order.

Security Notes

  • OTP codes are stored only as PBKDF2-SHA256 hashes with per-code salt.
  • OTP sessions are stored in SQLite, not process memory.
  • New OTP invalidates previous active OTP for the same phone and messenger identity.
  • Username, first name and display name are never used for authorization.
  • messengerUserId is preferred; customerExternalId is the primary BotMarketing stable identity; messengerAccountId and messengerAccountIdFallback are compatibility aliases; chatExternalId is auxiliary.
  • WhatsApp customer.external_id is stored as a string even when it looks like a phone number.
  • If a phone is not found in RetailCRM, the backend returns neutral status="sent" and creates a fake session without sending a code.
  • If several CRM customers are found by phone, /auth/start calls the configured deduper once and stores only the resolved finalCustomerId in the OTP session.
  • Conflicts in RetailCRM return status="conflict" and do not bind anything.
  • /auth/unlink never unlinks by external customerId alone; it first resolves the current messenger binding and clears only the mapped channel custom field.
  • /orders never uses external customerId as the access key; access is based on messenger binding only.
  • /orders/repeat never trusts external customerId, sourceOrderId, or sourceOrderNumber without checking them against the current messenger binding.

TODO / Integration Checks

  • Verify your real RetailCRM custom field codes and put them into RETAILCRM_CF_*.
  • Confirm whether your RetailCRM API key has read/write access to customers and custom fields.
  • Confirm your GreenSMS authentication mode: bearer token or user/pass.
  • Confirm approved GreenSMS sender names: GREENSMS_SMS_FROM and GREENSMS_VK_FROM.
  • GreenSMS docs expose cascade parameters but do not fully specify safe values for every route in the snippets used here. This backend performs application-level fallback to SMS only when the initial Telegram/VK request fails synchronously. If you need asynchronous fallback on delivery failure, add GreenSMS status polling or a documented cascade configuration after confirming the exact official parameters for your account.
  • RetailCRM phone lookup uses documented customer search plus exact local normalization against customer.phones[].number; if your account stores phone data differently, adjust RetailCRMClient.find_customers_by_phone.
  • Deduper integration assumes POST /api/dedupe/auto-resolve returns status, finalCustomerId, optional requestId, and optional warnings. If your deduper response shape differs, adjust app/services/deduper.py.
  • RetailCRM delivery integrations differ. /orders extracts tracking and address only from fields that are actually present in the order payload, such as delivery.code, delivery.integrationCode, delivery.trackNumber, delivery.trackingNumber, delivery.data.trackNumber, and delivery.address. If your delivery integration stores these values elsewhere, extend app/services/orders.py with that documented field path.
  • RetailCRM create-order validation can vary by catalog and delivery integration. /orders/repeat builds a minimal payload with customer id, current items by offer id/externalId/xmlId, quantities, delivery address, phone, name and email. If your account requires additional mandatory fields for bot-created orders, extend app/services/order_repeat.py.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors