Claude
Skills
Sign in
Back

podium-webchat-handler

Included with Lifetime
$97 forever

Ingest Podium webchat messages in production and survive the webchat-side failures — invalid phone formats accepted at the widget, contact auto-creation races producing duplicate records, session timeouts mid-conversation, attachment size overflows, cross-location chat routing wrong, and opt-out propagation lag. Use when hardening a webchat → API integration, building a multi-location chat widget, debugging duplicate contacts, or recovering from a cross-location routing incident. Trigger with "podium webchat", "podium chat widget", "podium phone validation", "podium contact dedup", "podium webchat session", "podium opt-out", "podium location routing".

Backend & APIspodiumwebchatphone-validatione164contact-dedupmulti-locationscripts

What this skill does


# Podium Webchat Handler

## Overview

Ingest Podium webchat messages into your production system and operate the webchat layer when it breaks. This is not a setup walkthrough — it is the handler code your integration runs at 11am on a Saturday when a Brisbane customer's webchat lands on the Sydney store's queue, when two simultaneous webchats from the same phone produce two duplicate contact records, when a customer types `1` of a `1-2-3` answer and the session dies before they finish, and when a customer types STOP and the next session five minutes later still tries to SMS them.

The six production failures this skill prevents:

1. **Invalid phone formats accepted** — Webchat asks for a phone number and a customer types `0412 345 678` (Australian local) or `(415) 555-1234` (US local) without E.164 normalization. The handler stores the local form. The later SMS reply attempt fails silently because Podium expects `+61412345678` / `+14155551234`. The agent thinks they replied; the customer never receives anything.
2. **Contact auto-creation race produces duplicates** — Two webchats arrive within milliseconds from the same phone (customer opens two tabs, or a webhook retry overlaps the first delivery). Both handlers check "does a contact with this phone exist?", both see no, both create — and now the same human is two contact records, with conversation history split across both.
3. **Webchat sessions time out mid-conversation** — Podium webchat sessions have a server-side expiry (default ~30 min idle). A customer types `1` of a `1-2-3` multiple-choice answer, walks away to grab lunch, comes back to finish, and discovers the agent picked up the conversation in fresh context without the `1` they sent.
4. **Attachment size overflows** — Podium accepts attachments up to 25MB. Webchat-to-API integrations that don't validate size client-side before upload fail server-side with a 413 — but only after the customer has waited through the upload progress bar. The customer thinks the image was sent; the agent never sees it.
5. **Cross-location chat routing is wrong** — A Sydney-based store and a Burleigh Heads–based store share the same Podium org. The webchat widget is embedded on a single corporate site and doesn't pass `location_uid` on the initial message. Every chat lands in the default location's queue regardless of which store the customer was actually browsing.
6. **Opt-out propagation lag** — A customer types STOP in an SMS thread. The opt-out flag is recorded in the SMS subsystem but not propagated to the webchat subsystem. Five minutes later the customer starts a new webchat session; the integration still tries to send an SMS confirmation reply and trips a compliance violation.

## Prerequisites

- Python 3.10+ (examples) or Node.js 18+
- `podium-auth` skill installed and a working `PodiumAuth` instance for OAuth token management
- `podium-webhook-reliability` skill installed if consuming webchat events via webhook (HMAC + dedup live there)
- `phonenumbers` library (Google's libphonenumber port): `pip install phonenumbers`
- Podium org with at least one location configured; for multi-location, the full `location_uid` list
- A contact store with a unique index on the normalized E.164 phone column (the natural dedup key)

## Instructions

Build in this order. Each section neutralizes one production failure mode.

### 1. E.164 phone normalization at the widget edge (neutralizes invalid phone formats)

Normalize phone numbers to E.164 **at the widget input boundary** before the message ever reaches your API. The widget knows the customer's locale context; the API does not. Use `phonenumbers` for the parse + validation:

```python
import phonenumbers
from phonenumbers import NumberParseException, PhoneNumberFormat, is_valid_number

class PhoneValidationError(Exception):
    pass

def normalize_phone(raw: str, default_country: str = "AU") -> str:
    """Parse a raw phone string and return E.164 form. Raises on invalid."""
    try:
        parsed = phonenumbers.parse(raw, default_country)
    except NumberParseException as e:
        raise PhoneValidationError(f"unparseable phone {raw!r}: {e}")
    if not is_valid_number(parsed):
        raise PhoneValidationError(f"invalid phone for region {default_country}: {raw!r}")
    return phonenumbers.format_number(parsed, PhoneNumberFormat.E164)

# Examples
assert normalize_phone("0412 345 678", "AU") == "+61412345678"
assert normalize_phone("(415) 555-1234", "US") == "+14155551234"
assert normalize_phone("+61 412 345 678", "AU") == "+61412345678"
```

The `default_country` parameter is the location's country (Sydney → AU, Burleigh Heads → AU, San Francisco → US). Pass it from the widget context, never hardcode globally. If the widget runs on a multi-region site and cannot determine the default, fail closed — refuse to accept the message until the customer enters a `+`-prefixed number explicitly.

### 2. Contact auto-creation race (neutralizes duplicate contact records)

The naive pattern — `if not contact_exists(phone): create_contact(phone)` — has a TOCTOU race. Under simultaneous webchat arrivals from the same phone, both branches see "no" and both create. The fix is **idempotent upsert keyed on the E.164 phone** with a unique index in the contact store, and retry-on-conflict semantics:

```python
import httpx
from podium_auth import PodiumAuth

async def upsert_contact_by_phone(
    auth: PodiumAuth,
    phone_e164: str,
    location_uid: str,
    first_name: str | None = None,
    last_name: str | None = None,
) -> dict:
    """Idempotent contact creation. Returns the contact record; never creates a duplicate."""
    token = await auth.get_token()
    headers = {"Authorization": f"Bearer {token}"}

    # Step 1: lookup by phone
    async with httpx.AsyncClient(timeout=10) as c:
        r = await c.get(
            "https://api.podium.com/v4/contacts",
            headers=headers,
            params={"phone": phone_e164, "location_uid": location_uid},
        )
    if r.status_code == 200 and r.json().get("data"):
        return r.json()["data"][0]

    # Step 2: create — but tolerate 409 conflict from a racing creator
    async with httpx.AsyncClient(timeout=10) as c:
        r = await c.post(
            "https://api.podium.com/v4/contacts",
            headers=headers,
            json={
                "phone": phone_e164,
                "location_uid": location_uid,
                "first_name": first_name,
                "last_name": last_name,
            },
        )
    if r.status_code in (200, 201):
        return r.json()
    if r.status_code == 409:
        # The race lost — refetch and return the winner's record
        async with httpx.AsyncClient(timeout=10) as c:
            r2 = await c.get(
                "https://api.podium.com/v4/contacts",
                headers=headers,
                params={"phone": phone_e164, "location_uid": location_uid},
            )
        if r2.status_code == 200 and r2.json().get("data"):
            return r2.json()["data"][0]
    raise WebchatError(f"contact upsert failed: {r.status_code} {r.text}")

class WebchatError(Exception):
    pass
```

In your local contact mirror (if you maintain one), enforce a database-level unique index on `phone_e164` so a parallel writer hits the constraint instead of silently double-inserting. The deeper mechanics — collision resolution when the same phone owns conflicting first/last names across sources — live in `podium-contact-dedup`. This skill prevents the most common race; that skill handles the harder reconciliation cases.

### 3. Webchat session timeout monitor (neutralizes mid-conversation context loss)

Podium webchat sessions have a server-side idle timeout. Detect approaching-expiry on your side and either prompt the customer to confirm they're still there, or buffer the partial answer so the agent picks up the conversation with context preserved:

```python
import time
from dataclasses import dataclass, fiel

Related in Backend & APIs