The challenge
Connecting a phone system to workflow automation looks like a single webhook. In practice, the SMS event on its own doesn’t carry everything the downstream workflow needs, so the service has to fetch the rest with follow-up REST calls. The raw data also includes sensitive fields that shouldn’t travel any further than necessary.
The service also had to be easy to run: deployable as a container, observable when something goes wrong, configurable without code changes and ready to feed downstream compliance and do-not-call (DNC) checks. A bridge that nobody can see into becomes the first suspect every time a workflow misbehaves.
On top of that, a naive bridge breaks in quieter ways:
- Webhook subscriptions lapse unless something renews them, and a lapsed subscription fails silently: nothing errors, the messages just stop arriving
- Transport retries can deliver the same event twice, which fires the same Zapier workflow twice
- Zapier or the network can fail mid-delivery, so forwarding needs retries with backoff, not a single attempt
- SMS data is personal data, so only what downstream steps need should leave the service
What I built
I built the service on FastAPI with async I/O throughout. Almost all of its work is waiting on other systems: receiving RingCentral’s webhook, calling its REST API for metadata and posting to Zapier. Async handling lets one small process keep many of those calls in flight without blocking, and HTTPX provides the async client for forwarding to Zapier.
A background renewer keeps the RingCentral webhook subscriptions alive. Without it, the integration works on day one and then quietly stops, which is the worst kind of failure for an automation nobody is watching.
Duplicates are handled by an in-memory idempotency filter. When a transport retry delivers an event the service has already forwarded, the filter suppresses it, so the retry doesn’t fire a second Zapier run. In-memory is the right size for a single container; if the service is ever scaled to several replicas, the same filter belongs in a shared store.
Masking happens before anything leaves the service. Sensitive fields are masked in the payload sent to Zapier, so third-party automation receives only what it needs. That is data minimisation in practice, and it gives the downstream compliance and DNC hooks a consistent, reduced payload to work from.
Configuration uses Pydantic settings, so credentials and endpoints come from the environment and are validated when the service starts rather than failing on the first message. Health endpoints let the container platform check the service is up, and structured logging records each step in a form that can be searched when someone asks what happened to a particular message.
Put together, every inbound event follows the same path:
- Receive the RingCentral SMS event on the subscribed webhook
- Hydrate it with follow-up REST calls for the metadata the event doesn’t include
- Mask sensitive fields
- Drop it if the idempotency filter has already seen it
- Forward a structured payload to Zapier, retrying with backoff on failure
Architecture and stack
- Service
- Python · FastAPI · Pydantic settings · HTTPX
- Integrations
- RingCentral · Zapier · Webhooks
- Operations
- Container deployment · Health endpoints · Structured logging
Outcome
The result is a small, single-purpose service that turns RingCentral SMS events into clean, enriched Zapier triggers. Subscriptions renew themselves, failed deliveries retry with backoff, duplicates from transport retries are filtered out, and sensitive fields are masked before data leaves the service.
Just as important, someone other than me can operate it. It deploys as a container, reports its health, logs in a structured way and keeps data to the minimum downstream steps need, which gives later compliance and DNC checks a consistent payload to work from.
- No silent failures from lapsed webhook subscriptions
- One Zapier run per message, even when transport retries resend an event
- Sensitive fields masked before data reaches third-party automation
- Container-ready, with health endpoints and structured logs
Last updated 2026-10-03