Skill: abhinavpgagi/toingg-skill (version 1.0.1)

--- name: toingg-skill description: End-to-end Toingg ops: create campaigns, (optionally) schedule daily analytics pulls, and turn Excel contact sheets into WhatsApp outreach via add_contacts + send_whatsapp_templates. Use when Codex needs to automate Toingg voice/WhatsApp workflows and requires scripts for campaign POSTs, analytics cron setup, or bulk contact uploads. --- # Toingg Ops Toolkit This skill bundles everything needed to manage Toingg campaigns from Claw: - **Campaign creation** via `create_campaign.py` - **Optional analytics cron** (7 PM daily) powered by `get_campaign_analytics.py` - **Contact upload + WhatsApp broadcast** using `xlsx_to_contacts.py`, `add_contacts.py`, and `send_whatsapp_templates.py` All HTTP calls reuse the `TOINGG_API_TOKEN` bearer token. ## Setup 1. Export your token in every environment that runs these scripts (gateway, cron, terminals). ```bash export TOINGG_API_TOKEN="tg_..." ``` 2. Install Python deps once if you will ingest Excel files: ```bash pip install openpyxl requests ``` 3. Keep payloads (campaign JSON, analytics snapshots, contact exports) in version control or shared storage per your security rules. ## Campaign creation workflow 1. Gather campaign fields from the user (title, voice, language, script, purpose, tone, post-call schema, notification numbers, autopilot flags, etc.). 2. Draft a payload JSON using [`references/payload-template.md`](references/payload-template.md) as the scaffold. 3. Run the helper: ```bash cd skills/toingg-skill ./scripts/create_campaign.py payloads/my_campaign.json > responses/create-$(date +%s).json ``` 4. Return the API response (campaign ID, status, or validation errors) to the user and log it. ## Opt-in analytics cron (7 PM daily) Only offer this when the user explicitly asks for daily analytics. 1. Confirm desired schedule/output directory. 2. Follow [`references/analytics-cron.md`](references/analytics-cron.md) to create `openclaw cron create toingg-analytics-digest ...` with the provided command snippet. Adjust paths if needed. 3. Double-check `TOINGG_API_TOKEN` is visible to the gateway before enabling the cron. 4. After the first run, share where the JSON snapshots live and how to disable the cron (`openclaw cron delete ...`). `get_campaign_analytics.py` can also be run ad-hoc for on-demand pulls: ```bash ./scripts/get_campaign_analytics.py > analytics.json ``` ## Contact upload + WhatsApp templates When a user supplies an Excel sheet (name / phone / context columns) and wants to blast a WhatsApp template: 1. **Convert Excel → JSON** ```bash ./scripts/xlsx_to_contacts.py ~/Downloads/leads.xlsx contacts.json ``` See [`references/contact-workflow.md`](references/contact-workflow.md) for the exact column expectations and troubleshooting. The script skips blank rows and normalizes phone numbers. 2. **Upload contacts** to a Toingg contact list (auto-creates if missing): ```bash ./scripts/add_contacts.py ClawTest contacts.json ``` 3. **Send WhatsApp templates** once the list is ready: ```bash ./scripts/send_whatsapp_templates.py \ 231565687 \ bfesfbgf \ en-US \ ClawTest \ --payload template-variables.json ``` - Omit `--payload` (defaults to `[]`) if the template has no variables. - Pass `--resend` only when the user explicitly wants to re-contact existing recipients. 4. Confirm delivery status in Toingg and report any errors back to the user (the helper prints full JSON responses for logging). ## File map | Script | Purpose | |--------|---------| | `scripts/create_campaign.py` | POST `/api/v3/create_campaign` with arbitrary payloads | | `scripts/get_campaign_analytics.py` | GET `/api/v3/get_campaign_analytics` (cron-friendly) | | `scripts/xlsx_to_contacts.py` | Convert Excel sheets into Toingg contact JSON | | `scripts/add_contacts.py` | Upload contact lists via `/api/v3/add_contacts` | | `scripts/send_whatsapp_templates.py` | Trigger `/api/v3/send_whatsapp_templates` | Keep this toolkit lightweight: update the references when Toingg adds new fields or workflows so other operators can follow the same patterns.

path: references/analytics-cron.md
content: # Analytics Cron Playbook

1. **Prerequisites**
   - `TOINGG_API_TOKEN` exported in the environment used by the OpenClaw Gateway.
   - `skills/toingg-skill/scripts/get_campaign_analytics.py` committed to the workspace.

2. **Create a destination folder** for daily snapshots (optional but recommended):
   ```bash
   mkdir -p ~/toingg-analytics
   ```

3. **Create the cron job** (fires at 19:00 local time):
   ```bash
   openclaw cron create toingg-analytics-digest \
     --schedule "0 19 * * *" \
     --command "cd /Users/abhinavkalvacherla/.openclaw/workspace/skills/toingg-skill && ./scripts/get_campaign_analytics.py > ~/toingg-analytics/analytics-$(date +%Y%m%d).json"
   ```

   - Edit the output path to wherever you want the JSON stored.
   - Cron inherits the environment from the gateway; confirm `TOINGG_API_TOKEN` is set there.

4. **Enable only when requested**:
   - Before creating the cron, confirm the user wants scheduled analytics.
   - To pause or remove it later:
     ```bash
     openclaw cron delete toingg-analytics-digest
     ```

5. **Notify the user** after the first successful run and share where the files live.


path: references/contact-workflow.md
content: # Contact Upload + WhatsApp Template Workflow

## Excel format

Create a spreadsheet with **exactly these headers in the first row** (case-insensitive):

| name | phone | context |
|------|-------|---------|

- `name`: Display name for the contact.
- `phone`: Digits only, include country code (no `+`).
- `context`: Free-form string passed into `extraParams.context`.

Save the file as `.xlsx`.

## Converting to JSON

Use the helper to convert the spreadsheet into the API payload:

```bash
cd skills/toingg-skill
./scripts/xlsx_to_contacts.py ~/Downloads/contacts.xlsx contacts.json
```

The script requires `openpyxl`. Install once if missing:

```bash
pip install openpyxl
```

`contacts.json` will contain an array like:

```json
[
  {"name": "Abhinav", "phone": "918179259307", "extraParams": {"context": "PGAGI"}},
  {"name": "Bibin", "phone": "918179259307", "extraParams": {"context": "PGAGI"}}
]
```

## Uploading to Toingg

```bash
./scripts/add_contacts.py ClawTest contacts.json
```

- `ClawTest` is the target contact list name (create-on-write semantics).
- Command prints how many contacts were uploaded.

## Sending WhatsApp templates

```bash
./scripts/send_whatsapp_templates.py \
  231565687 \
  bfesfbgf \
  en-US \
  ClawTest \
  --payload template-vars.json
```

- `template-vars.json` should be a JSON array (default `[]` if not needed).
- Add `--resend` to deliver again to contacts who already received the template.

After sending, monitor delivery from the Toingg dashboard.


path: references/payload-template.md
content: # Toingg Campaign Payload Template

Use this template when constructing the JSON passed to `create_campaign.py`. Remove
any keys you do not need; unspecified values follow Toingg defaults.

```json
{
  "title": "testclaw",
  "voice": "emily",
  "language": "english",
  "script": "",
  "purpose": "You are a Sales Expert and your goals are to Cold lead qualification for Cold Leads in a Professional tone.",
  "knowledgeBase": "",
  "calendar": "",
  "firstLine": "",
  "flowName": "",
  "systemPrompt": "",
  "tone": "Professional",
  "maxCallTimeout": 3,
  "inactiveCallTimeout": 15,
  "postCallAnalysis": true,
  "postCallAnalysisSchema": {
    "lead_type": "Classify leads as Hot, Warm, or Cold based on buying intent",
    "location": "Auto-extract geographic location from conversation",
    "customer_sentiment": "Track emotional tone throughout the conversation"
  },
  "successParameter": {
    "successKey": "",
    "desc": "",
    "success_fields": []
  },
  "totalCallTime": 0,
  "totalNumCall": 0,
  "totalSpent": 0,
  "totalCallSpent": 0,
  "totalTextSpent": 0,
  "averageCallCost": 0,
  "timeZone": "UTC",
  "isFreeflow": true,
  "public": false,
  "sampleSid": "",
  "description": "",
  "asr": "AZURE",
  "startMessage": true,
  "clearMemory": false,
  "leadNotificationPhone": "+918179259307",
  "leadNotification": true,
  "campaignType": {
    "textPipeline": false,
    "callPipeline": true
  },
  "campaignMode": {
    "inbound": false,
    "outbound": true
  },
  "integrations": [
    {
      "provider": "handoff",
      "providerType": "handoff",
      "enabled": true
    }
  ],
  "autoPilot": true,
  "humanHandoff": true
}
```

## Field notes

- **title** *(string, required)* — Human-friendly campaign name.
- **voice/language/tone** — Must match choices available inside Toingg.
- **script/systemPrompt/purpose** — Provide the conversational copy and guardrails.
- **postCallAnalysisSchema** — Define structured outputs you want Toingg to return.
- **leadNotificationPhone** — E.164-formatted phone number for alerts.
- **campaignType/campaignMode** — Control whether the campaign makes outbound calls.
- **integrations** — Enable downstream workflows (handoff, CRMs, etc.).

Any additional keys supported by the API can be added; the backend ignores unkown
fields that are not part of the schema.


path: scripts/add_contacts.py
content: #!/usr/bin/env python3
"""Upload contacts to a Toingg contact list."""
from __future__ import annotations

import argparse
import json
import os
import sys
import textwrap
from pathlib import Path
from typing import Any, List

import requests

API_URL = "https://prepodapi.toingg.com/api/v3/add_contacts"
ENV_TOKEN_KEY = "TOINGG_API_TOKEN"


def load_contacts(path: Path) -> List[Any]:
    try:
        with open(path, "r", encoding="utf-8") as fh:
            data = json.load(fh)
    except FileNotFoundError as exc:
        raise SystemExit(f"Contacts file not found: {path}") from exc
    except json.JSONDecodeError as exc:
        raise SystemExit(f"Invalid JSON in {path}: {exc}") from exc

    if not isinstance(data, list):
        raise SystemExit("Contacts JSON must be an array")
    return data


def add_contacts(contact_list: str, contacts: List[Any]) -> None:
    [REDACTED_SECRET])
    if not token:
        raise SystemExit(
            textwrap.dedent(
                f"""
                Missing {ENV_TOKEN_KEY}. Export your Toingg bearer token, e.g.:
                  export {ENV_TOKEN_KEY}=<token>
                """
            ).strip()
        )

    url = f"{API_URL}?contactList={contact_list}"
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/json",
        "Content-Type": "application/json",
    }
    response = requests.post(url, headers=headers, json=contacts, timeout=60)
    try:
        response.raise_for_status()
    except requests.HTTPError as exc:
        raise SystemExit(
            f"add_contacts request failed: {exc}\nResponse body:\n{response.text}"
        ) from exc

    print(f"Uploaded {len(contacts)} contacts to list '{contact_list}'.")


def main() -> None:
    parser = argparse.ArgumentParser(description="Create/update a Toingg contact list")
    parser.add_argument("contact_list", help="Name of the contact list (e.g., ClawTest)")
    parser.add_argument(
        "contacts_json",
        help="Path to JSON array produced by xlsx_to_contacts.py",
    )
    args = parser.parse_args()

    contacts = load_contacts(Path(args.contacts_json))
    add_contacts(args.contact_list, contacts)


if __name__ == "__main__":
    main()


path: scripts/create_campaign.py
content: #!/usr/bin/env python3
"""Create a Toingg campaign via the public API."""
import argparse
import json
import os
import sys
import textwrap
from typing import Any, Dict

import requests

API_URL = "https://prepodapi.toingg.com/api/v3/create_campaign"
ENV_TOKEN_KEY = "TOINGG_API_TOKEN"


def load_payload(path: str) -> Dict[str, Any]:
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError as exc:
        raise SystemExit(f"Payload file not found: {path}") from exc
    except json.JSONDecodeError as exc:
        raise SystemExit(f"Invalid JSON in payload file {path}: {exc}") from exc


def create_campaign(payload: Dict[str, Any]) -> Dict[str, Any]:
    [REDACTED_SECRET])
    if not token:
        raise SystemExit(
            textwrap.dedent(
                f"""
                Missing {ENV_TOKEN_KEY} environment variable.
                Export your Toingg bearer token before running this script, e.g.:
                  export {ENV_TOKEN_KEY}=your_token_here
                """
            ).strip()
        )

    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/json",
        "Content-Type": "application/json",
    }

    response = requests.post(API_URL, headers=headers, json=payload, timeout=60)
    try:
        response.raise_for_status()
    except requests.HTTPError as exc:
        details = response.text
        raise SystemExit(
            f"Request failed: {exc}. Response body:\n{details}"
        ) from exc

    try:
        return response.json()
    except json.JSONDecodeError:
        return {"raw": response.text}


def main() -> None:
    parser = argparse.ArgumentParser(description="Create a Toingg campaign")
    parser.add_argument(
        "payload",
        help="Path to a JSON file that matches the Toingg create_campaign schema",
    )
    args = parser.parse_args()

    payload = load_payload(args.payload)
    result = create_campaign(payload)
    json.dump(result, sys.stdout, indent=2)
    sys.stdout.write("\n")


if __name__ == "__main__":
    main()


path: scripts/get_campaign_analytics.py
content: #!/usr/bin/env python3
"""Fetch Toingg campaign analytics and print JSON to stdout."""
from __future__ import annotations

import json
import os
import sys
import textwrap
from typing import Any, Dict

import requests

API_URL = "https://prepodapi.toingg.com/api/v3/get_campaign_analytics"
ENV_TOKEN_KEY = "TOINGG_API_TOKEN"


def fetch_analytics() -> Dict[str, Any]:
    [REDACTED_SECRET])
    if not token:
        raise SystemExit(
            textwrap.dedent(
                f"""
                Missing {ENV_TOKEN_KEY}. Export your Toingg bearer token, e.g.:
                  export {ENV_TOKEN_KEY}=<token>
                """
            ).strip()
        )

    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/json",
    }
    response = requests.get(API_URL, headers=headers, timeout=60)
    try:
        response.raise_for_status()
    except requests.HTTPError as exc:
        raise SystemExit(
            f"Analytics request failed: {exc}\nResponse body:\n{response.text}"
        ) from exc

    try:
        return response.json()
    except json.JSONDecodeError:
        return {"raw": response.text}


def main() -> None:
    data = fetch_analytics()
    json.dump(data, sys.stdout, indent=2)
    sys.stdout.write("\n")


if __name__ == "__main__":
    main()


path: scripts/send_whatsapp_templates.py
content: #!/usr/bin/env python3
"""Trigger Toingg WhatsApp templates for a contact list."""
from __future__ import annotations

import argparse
import json
import os
import sys
import textwrap
from typing import Any, List

import requests

API_URL = "https://prepodapi.toingg.com/api/v3/send_whatsapp_templates"
ENV_TOKEN_KEY = "TOINGG_API_TOKEN"


def send_templates(
    campaign_id: str,
    template_name: str,
    lang_code: str,
    contact_list: str,
    resend: bool,
    payload: List[Any],
) -> Any:
    [REDACTED_SECRET])
    if not token:
        raise SystemExit(
            textwrap.dedent(
                f"""
                Missing {ENV_TOKEN_KEY}. Export your Toingg bearer token, e.g.:
                  export {ENV_TOKEN_KEY}=<token>
                """
            ).strip()
        )

    params = {
        "campaignId": campaign_id,
        "templateName": template_name,
        "langCode": lang_code,
        "contactList": contact_list,
        "resend": str(resend).lower(),
    }
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/json",
        "Content-Type": "application/json",
    }
    response = requests.post(API_URL, headers=headers, params=params, json=payload, timeout=60)
    try:
        response.raise_for_status()
    except requests.HTTPError as exc:
        raise SystemExit(
            f"send_whatsapp_templates failed: {exc}\nResponse body:\n{response.text}"
        ) from exc

    try:
        return response.json()
    except json.JSONDecodeError:
        return {"raw": response.text}


def main() -> None:
    parser = argparse.ArgumentParser(description="Send WhatsApp templates via Toingg")
    parser.add_argument("campaign_id", help="Toingg campaign ID")
    parser.add_argument("template_name", help="WhatsApp template name")
    parser.add_argument("lang_code", help="Template language code (e.g., en-US)")
    parser.add_argument("contact_list", help="Contact list name")
    parser.add_argument(
        "--resend",
        action="store_true",
        help="Set if re-sending to contacts who already received the template",
    )
    parser.add_argument(
        "--payload",
        help="Optional JSON array of template variables (default: [])",
    )
    args = parser.parse_args()

    payload: List[Any] = []
    if args.payload:
        try:
            with open(args.payload, "r", encoding="utf-8") as fh:
                payload = json.load(fh)
        except FileNotFoundError as exc:
            raise SystemExit(f"Payload file not found: {args.payload}") from exc
        except json.JSONDecodeError as exc:
            raise SystemExit(f"Invalid JSON payload: {exc}") from exc
        if not isinstance(payload, list):
            raise SystemExit("Payload must be a JSON array")

    result = send_templates(
        campaign_id=args.campaign_id,
        template_name=args.template_name,
        lang_code=args.lang_code,
        contact_list=args.contact_list,
        resend=args.resend,
        payload=payload,
    )
    json.dump(result, sys.stdout, indent=2)
    sys.stdout.write("\n")


if __name__ == "__main__":
    main()


path: scripts/xlsx_to_contacts.py
content: #!/usr/bin/env python3
"""Convert an Excel contact sheet into the JSON payload required by Toingg."""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import List, Dict, Any

try:
    from openpyxl import load_workbook
except ImportError as exc:  # pragma: no cover
    raise SystemExit(
        "openpyxl is required for xlsx parsing. Install with `pip install openpyxl`."
    ) from exc

REQUIRED_COLUMNS = {"name", "phone", "context"}


def normalize_header(header: str | None) -> str:
    return (header or "").strip().lower()


def sheet_to_contacts(path: Path) -> List[Dict[str, Any]]:
    wb = load_workbook(path, read_only=True, data_only=True)
    ws = wb.active

    rows = list(ws.iter_rows(values_only=True))
    if not rows:
        raise SystemExit("Excel file is empty")

    headers = [normalize_header(str(cell)) for cell in rows[0]]
    missing = REQUIRED_COLUMNS - set(headers)
    if missing:
        raise SystemExit(
            f"Missing required columns: {', '.join(sorted(missing))}."
            " Ensure the first row has name, phone, context."
        )

    idx = {header: i for i, header in enumerate(headers)}
    contacts: List[Dict[str, Any]] = []
    for row in rows[1:]:
        if row is None:
            continue
        name_value = row[idx["name"]] if idx["name"] < len(row) else None
        phone_value = row[idx["phone"]] if idx["phone"] < len(row) else None
        context_value = row[idx["context"]] if idx["context"] < len(row) else None

        name = str(name_value).strip() if name_value is not None else ""
        if isinstance(phone_value, (int, float)):
            phone = str(int(phone_value)).strip()
        else:
            phone = str(phone_value).strip() if phone_value is not None else ""
        context = str(context_value).strip() if context_value is not None else ""

        if not name or not phone:
            continue
        contacts.append(
            {
                "name": name,
                "phone": phone,
                "extraParams": {"context": context},
            }
        )
    if not contacts:
        raise SystemExit("No valid contacts found in spreadsheet")
    return contacts


def main() -> None:
    parser = argparse.ArgumentParser(description="Convert xlsx contacts to JSON")
    parser.add_argument("xlsx", help="Path to Excel file with name/phone/context columns")
    parser.add_argument(
        "output",
        nargs="?",
        default="contacts.json",
        help="Where to write the JSON array (default: contacts.json)",
    )
    args = parser.parse_args()

    contacts = sheet_to_contacts(Path(args.xlsx))
    with open(args.output, "w", encoding="utf-8") as fh:
        json.dump(contacts, fh, indent=2)
        fh.write("\n")
    print(f"Wrote {len(contacts)} contacts to {args.output}")


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (HIGH)