integrations
July 27, 2026

Customer.io tracks every email open, click, unsubscribe, and delivery failure for your campaigns. You can see aggregate stats in its dashboard, but the moment you want to join that data against your own product usage tables, cohort it by signup date, or build a churn model, you're stuck. Customer.io's reporting UI doesn't do arbitrary SQL, and there's no scheduled export that keeps pace with events as they happen.
ProxyHook closes that gap. Customer.io already supports outbound Reporting Webhooks, so pointing them at a ProxyHook Source and routing to a Postgres Destination gets every engagement event into a table you control, in real time.
Reporting Webhooks are configured under Data & Integrations > Reporting Webhooks in your Customer.io account. When you add a webhook, you check the notification types you want delivered: email sent, delivered, opened, clicked, bounced, and customer subscribed or unsubscribed, among others. Customer.io POSTs one event per notification to your endpoint as it happens.
A clicked-email event looks roughly like this:
{
"event_id": "01H8X8K7QK6J4N",
"object_type": "email",
"metric": "clicked",
"data": {
"customer_id": "4471",
"delivery_id": "cio_a1b2c3d4",
"campaign_id": 87,
"subject": "Your weekly digest",
"href": "https://app.example.com/dashboard"
},
"timestamp": 1753650012
}
The metric field is the important one for routing decisions. It's the same field every notification type carries, whether the event is opened, clicked, bounced, or customer_subscribed, so it's what you'll filter on downstream.
In your ProxyHook dashboard, create a new Source and select Customer.io. You'll get a unique ingest endpoint:
https://go.proxyhook.com/A817GH
ProxyHook acknowledges each POST immediately, so a slow moment on the Postgres side never causes Customer.io to back off or drop a delivery.
In your Customer.io account, go to Data & Integrations > Reporting Webhooks > Add Webhook. Paste the ProxyHook Source URL into the endpoint field, then check the notification types you want to receive. If you only care about engagement, not delivery mechanics, check email_opened, email_clicked, customer_subscribed, and customer_unsubscribed, and skip email_sent and email_delivered to keep volume down.
Add a new Destination, select Postgres, and enter your connection credentials. Once it's created, the Destination's Activity view starts tracking total requests, 2XX vs. non-2XX responses, filtered requests, and average execution time, sliceable from 30 minutes to a month.
From the Automations tab, create a new Automation, pick your Customer.io Source, and add the Postgres Destination. Save it and events start flowing as soon as the next campaign fires.
If you checked every notification type in Customer.io, you'll get email_sent and email_delivered rows alongside the engagement events you care about, and those tend to outnumber opens and clicks by a wide margin on any list of real size. Add a Payload Contents filter step in the Automation, between the Source and the Postgres Destination:
metricsentAdd a second condition with not equals and delivered to drop both noise events. What reaches Postgres is opens, clicks, bounces, and subscription changes, the events that actually inform a cohort or churn query.
If you only want to track deliverability failures separately, flip the logic: a second Postgres Destination in the same Automation with metric equals bounced gives you a dedicated bounce table without touching the engagement pipeline.
Every Reporting Webhook payload shares the outer shape (event_id, object_type, metric, timestamp), but the data object's fields shift depending on the metric: a clicked event carries href, a customer_subscribed event doesn't. Two approaches both work:
A single table with metric as a plain column and data as jsonb handles this without a migration every time Customer.io adds a field. Index metric and data->>'customer_id' and you can query "how many times did customer 4471 click in the last 30 days" without touching schema. customer_id is present on every event type, which makes it the natural join key back to your own users table.
If you need fast aggregate queries over a specific metric at scale, a normalized email_clicks table with customer_id, campaign_id, and href as real columns will outperform jsonb extraction on click-through-rate reports run over millions of rows. Route clicked events to that table with a second Postgres Destination and a matching filter, while the catch-all jsonb table keeps everything else.
Two views tell you where the pipeline is stuck. Open the Postgres Destination's Activity tab first: a spike in non-2XX responses means ProxyHook is reaching your database but the write is failing, usually a schema mismatch (a new metric type with an unexpected data shape) or a connection limit hit during a campaign send that fires thousands of events at once.
If Activity shows zero total deliveries instead of failures, check the Source's Logs view. It captures the raw request and response for every event that hit the endpoint. No rows there for the time window means Customer.io never sent anything, which usually traces back to the webhook being disabled or the wrong notification types being unchecked in Data & Integrations > Reporting Webhooks.
You can replay any logged event from the Source Logs view to re-run it through the Automation and its filters. That's the fastest way to confirm a filter change is behaving correctly: find a clicked event you've already received, replay it after editing the filter, and check whether it lands in Postgres without waiting for a real click.
Create a Customer.io Source in ProxyHook, add a Reporting Webhook in Customer.io pointed at that Source's endpoint, add a Postgres Destination with your connection credentials, and connect them with an Automation. Filter out sent and delivered if you only want engagement data, and you'll have a queryable table of opens, clicks, and unsubscribes updating in real time.
View the Customer.io integration | View the Postgres integration