Metadata-Version: 2.5
Name: flowsell-connect
Version: 0.2.0
Summary: Official Python SDK for FlowSell Connect unified communication APIs.
Project-URL: Homepage, https://connect.flowsell.app/sdk
Project-URL: Documentation, https://connect.flowsell.app/sdk
Project-URL: Repository, https://github.com/flowsell-connect/flowsell-connect-python-sdk
Project-URL: Issues, https://github.com/flowsell-connect/flowsell-connect-python-sdk/issues
Project-URL: Package, https://pypi.org/project/flowsell-connect/
Author: FlowSell
License-Expression: MIT
License-File: LICENSE
Keywords: api,connect,flowsell,messaging,sdk,whatsapp
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# FlowSell Connect Python SDK

Official Python SDK for FlowSell Connect unified communication APIs.

- Package: https://pypi.org/project/flowsell-connect/
- Repository: https://github.com/flowsell-connect/flowsell-connect-python-sdk
- Documentation: https://connect.flowsell.app/sdk

## Install

```bash
pip install flowsell-connect
```

## Configure

```bash
export FLOWSELL_API_KEY=fsc_your_secret
export FLOWSELL_API_BASE_URL=https://cmwhd2hdarqpip3krnua6ep3le0qhstm.lambda-url.ap-south-1.on.aws
export FLOWSELL_PHONE_NUMBER_ID=1131858666672190
```

## Usage

```py
from flowsell_connect import FlowSell

client = FlowSell(api_key="fsc_your_secret")

client.messages.send({
    "channel": "whatsapp",
    "phoneNumberId": "1131858666672190",
    "to": "+919999999999",
    "message": "Hello from FlowSell Connect Python SDK",
})
```

## Templates

```py
client.templates.create({
    "channel": "whatsapp",
    "phoneNumberId": "1131858666672190",
    "name": "order_update",
    "category": "UTILITY",
    "content": "Hello {{customer_name}}\nYour order {{order_id}} has shipped.",
    "sampleVariables": {
        "customer_name": "John",
        "order_id": "#1234",
    },
})

client.messages.send_template({
    "channel": "whatsapp",
    "phoneNumberId": "1131858666672190",
    "to": "+919999999999",
    "template": "order_update",
    "variables": {
        "customer_name": "John",
        "order_id": "#1234",
    },
})
```

## Messaging helpers

```py
to = "+919999999999"
client.messages.send_reaction({"to": to, "messageId": "wamid.HBg...", "emoji": "👍"})
client.messages.send_location({"to": to, "latitude": 19.07, "longitude": 72.87, "name": "Store"})
client.messages.send_contacts({"to": to, "contacts": [{"name": {"formatted_name": "John Doe"}, "phones": [{"phone": to}]}]})
client.messages.send_interactive_buttons({"to": to, "body": "Confirm?", "buttons": [{"id": "yes", "title": "Yes"}, {"id": "no", "title": "No"}]})
client.messages.send_list({"to": to, "body": "Pick a plan", "buttonText": "Plans", "sections": [{"title": "Plans", "rows": [{"id": "pro", "title": "Pro"}]}]})
client.messages.send_cta_url({"to": to, "body": "Track your order", "url": "https://example.com/track/1", "displayText": "Track"})
client.messages.send_flow({"to": to, "body": "Book a slot", "flowId": "1234567890", "flowCta": "Book"})
client.messages.send_media({"to": to, "type": "image", "link": "https://example.com/a.jpg"})
client.messages.mark_read({"messageId": "wamid.HBg..."})
client.messages.typing({"messageId": "wamid.HBg..."})  # read receipt + typing indicator
```

Method names are snake_case; request fields stay camelCase exactly as in the REST docs.

## Metering

- Every `/v1` call needs an active FlowSell Connect subscription.
- Writes (POST/DELETE calls that change something at Meta, such as sending, registering a number, creating a QR code or a flow) consume **1 request unit** from your monthly plan quota and appear in `client.usage.history()`.
- Read-only GET calls are free (they still need an active subscription).
- Paid template sends are additionally charged to the Meta wallet for **FlowSell-funded** numbers. Customer-funded numbers are billed by Meta directly. Use `client.billing.estimate()` before a campaign.

## Onboarding via API

```py
config = client.onboarding.config()
if not config["requirements"]["ready"]:
    print(config["requirements"]["blockers"], config["requirements"]["billingUrl"])
```

### 1. Hosted link

Create a session and send the business to `session["url"]`. FlowSell runs Meta Embedded Signup on its own page and redirects to `redirectUrl` with `status`, `session_id`, `account_id`, `phone_number_id` and `reference`.

```py
session = client.onboarding.create_session({
    "fundingMode": "CUSTOMER_FUNDED",  # or "FLOWSELL_FUNDED"
    "reference": "customer_42",
    "redirectUrl": "https://app.example.com/whatsapp/connected",
})
print(session["url"])

latest = client.onboarding.get_session(session["id"])  # or listen for account.connected
client.onboarding.list_sessions({"status": "PENDING"})
client.onboarding.cancel_session(session["id"])
```

### 2. Self-hosted Embedded Signup

Run Embedded Signup in your page with `config["embeddedSignup"]`, then send the `code`, `waba_id` and `phone_number_id` to your server:

```py
account = client.onboarding.complete({"code": code, "wabaId": waba_id, "phoneNumberId": phone_number_id})
```

### 3. Business login

```py
wabas = client.onboarding.business_login({"code": code, "redirectUri": redirect_uri})
client.onboarding.complete({"metaConnectionId": meta_connection_id, "wabaId": waba_id, "phoneNumberId": phone_number_id})
```

## Account management

```py
accounts = client.accounts.list({"status": "ACTIVE"})["data"]
client.accounts.get(accounts[0]["id"])  # id or phoneNumberId
client.accounts.health(accounts[0]["id"])
client.accounts.resubscribe_webhooks(accounts[0]["id"])
client.accounts.disconnect(accounts[0]["id"])
client.waba.get({"phoneNumberId": phone_number_id})
client.waba.subscribed_apps()
```

## Phone numbers

```py
client.phone_numbers.list()
client.phone_numbers.get(phone_number_id, {"fields": "id,display_phone_number,quality_rating"})
client.phone_numbers.request_code(phone_number_id, {"codeMethod": "SMS", "language": "en_US"})
client.phone_numbers.verify_code(phone_number_id, {"code": "123456"})
client.phone_numbers.register(phone_number_id, {"pin": "654321"})
client.phone_numbers.set_two_step_pin(phone_number_id, {"pin": "654321"})
client.phone_numbers.deregister(phone_number_id)
client.phone_numbers.get_settings(phone_number_id)
client.phone_numbers.update_settings(phone_number_id, {"calling": {"status": "ENABLED"}})
client.phone_numbers.update_conversational_automation(phone_number_id, {"enableWelcomeMessage": True, "prompts": ["Track my order"]})
client.phone_numbers.update_commerce_settings(phone_number_id, {"isCartEnabled": True, "isCatalogVisible": True})
client.phone_numbers.set_encryption_key(phone_number_id, {"businessPublicKey": public_key_pem})
client.phone_numbers.coexistence_sync(phone_number_id, {"syncType": "history"})
```

## Business profile, uploads, QR codes, blocked users

```py
client.business_profile.get({"phoneNumberId": phone_number_id})
client.business_profile.update({"about": "Fast delivery", "vertical": "GROCERY", "profilePictureUrl": "https://example.com/logo.png"})
handle = client.uploads.create_handle({"url": "https://example.com/sample.png"})["handle"]

qr = client.qr_codes.create({"prefilledMessage": "Hi! I want to order", "format": "PNG"})
client.qr_codes.list()
client.qr_codes.get(qr["code"])
client.qr_codes.update(qr["code"], {"prefilledMessage": "Hi! Show me the menu"})
client.qr_codes.delete(qr["code"])

client.block_users.block({"users": ["+919999999999"]})
client.block_users.list({"limit": 50})
client.block_users.unblock({"users": ["+919999999999"]})
```

## Media, templates, calls

```py
uploaded = client.media.upload_base64({"data": base64_png, "mimeType": "image/png", "filename": "logo.png"})
client.media.info(uploaded["id"])
file = client.media.download(uploaded["id"])  # MediaDownload(data=bytes, mime_type, sha256), up to 4 MB
client.media.delete(uploaded["id"])

client.templates.search({"status": "APPROVED", "limit": 50})
client.templates.get_by_id("1234567890")
client.templates.edit("1234567890", {"components": [...]})
client.templates.library({"search": "order"})
client.templates.create_from_library({"name": "order_shipped", "libraryTemplateName": "shipment_confirmation_2"})
client.templates.delete_by_name("order_update", {"templateId": "1234567890"})

client.calls.action({"action": "terminate", "callId": call_id})
```

## Flows

```py
flow = client.flows.create({"name": "Lead form", "categories": ["LEAD_GENERATION"], "flowJson": flow_json})
client.flows.update_json(flow["id"], {"flowJson": flow_json})
client.flows.preview(flow["id"], {"invalidate": True})
client.flows.publish(flow["id"])
client.flows.list({"limit": 25})
client.flows.get(flow["id"])
client.flows.assets(flow["id"])
client.flows.update(flow["id"], {"name": "Lead form v2"})
client.flows.deprecate(flow["id"])
client.flows.delete(flow["id"])  # drafts only
```

## Analytics

`start` / `end` accept unix seconds, unix milliseconds or ISO dates; list filters accept Python lists.

```py
client.analytics.messages({"start": "2026-09-01", "end": "2026-09-30", "granularity": "DAY"})
client.analytics.conversations({"granularity": "DAILY", "dimensions": ["CONVERSATION_CATEGORY"]})
client.analytics.pricing({"granularity": "MONTHLY", "countryCodes": ["IN"]})
client.analytics.enable_template_insights()
client.analytics.templates({"templateIds": ["1234567890"], "metricTypes": ["SENT", "READ"]})
```

## Billing

```py
client.billing.subscription()
client.billing.wallet()
client.billing.ledger({"limit": 50})
client.billing.rates({"countryCode": "IN", "category": "marketing"})
estimate = client.billing.estimate({"to": "+919999999999", "category": "marketing", "count": 1000})
client.usage.history({"start": "2026-09-01", "end": "2026-10-01"})
```

## Webhooks

```py
created = client.webhooks.create({"url": "https://app.example.com/webhooks/flowsell", "events": ["message.inbound", "message.status", "account.*"]})
secret = created["signingSecret"]  # shown once
client.webhooks.list()
client.webhooks.update(created["endpoint"]["id"], {"isActive": False})
client.webhooks.rotate_secret(created["endpoint"]["id"])
client.webhooks.deliveries({"status": "FAILED"})
client.webhooks.retry_delivery(delivery_id)
client.webhooks.delete(created["endpoint"]["id"])  # deactivates
```

Verify each delivery against the raw body (Flask example):

```py
from flowsell_connect import verify_webhook_signature

@app.post("/webhooks/flowsell")
def flowsell_webhook():
    if not verify_webhook_signature(
        request.get_data(),
        request.headers.get("x-flowsell-signature"),
        os.environ["FLOWSELL_WEBHOOK_SECRET"],
        request.headers.get("x-flowsell-timestamp"),
    ):
        return "", 401
    event = request.get_json()
    return "", 200
```

The signature is `sha256=` + hex HMAC-SHA256 of `f"{timestamp}.{raw_body}"`. Deliveries older than 5 minutes are rejected by default (`tolerance_seconds=` to change).

## Examples

```bash
FLOWSELL_API_KEY=fsc_your_secret python examples/send_message.py
FLOWSELL_API_KEY=fsc_your_secret python examples/onboarding.py
FLOWSELL_API_KEY=fsc_your_secret python examples/phone_numbers.py
FLOWSELL_API_KEY=fsc_your_secret python examples/billing.py
```
