Proxying requests to Customer.io
UpdatedProxying requests through your backend server lets you keep your Customer.io credentials secure while maintaining control over data collection. Instead of sending data directly from client applications to Customer.io, your SDKs send data to a server you control, which then forwards the requests to Customer.io.
Why proxy requests?
There are several reasons you might want to proxy requests through your backend:
- Security: Keep your Customer.io credentials secure on your backend instead of embedding them in client applications.
- Privacy and compliance: Route requests through your own domain to provide better privacy controls to meet app store requirements or avoid 3rd-party blockers.
- Enrich data: If necessary, you can transform, enrich, or filter requests before you send them to Customer.io.
General setup process
You’ll need to do a few things to proxy requests from your client to Customer.io:
- Configure your client (mobile app, website, etc.) to send requests to your backend instead of Customer.io.
- Set up your backend to receive requests, extract authentication tokens, and validate them.
- Forward events to Customer.io by parsing events and sending them with your actual Customer.io credentials.
- Handle incoming requests by tying together authentication, validation, and forwarding in your request handler.
1. Configure your client
Configure your SDK to send requests to your backend instead of Customer.io. The exact configuration settings you need to change depend on whether you use our JavaScript integration for your website or one of our mobile SDKs.
For our JavaScript client see our JavaScript proxy instructions. The way you invoke a proxy depends on whether you use our JavaScript snippet or import the
cdp-analytics-browserlibrary.For our Mobile SDKs you’ll set
apiHostandcdnHostsettings when you initialize the SDK. See:
In any case, you’ll set an authorization key. Normally this would be something you get from Customer.io. But, when you use a proxy, you can use any key that you can decode into basic authorization credentials when requests hit your proxy server. We’ll discuss that in more detail later on this page.
2. Set up your backend
Your backend needs to receive requests from your client SDKs and authenticate them using whatever custom authorization value you provide. (You can simply send your Customer.io credentials, but this would defeat the added security of the proxy!) The SDK sends a base64-encoded Authorization header as Basic <base64-encoded-value>.
Your backend needs to:
- Initialize a server and Customer.io SDK client with your actual credentials (stored securely)
- Extract the authentication token from incoming requests
- Validate the token against your authentication system
- Prepare a valid credential for the requests you forward to Customer.io
Here’s a complete implementation showing server setup and authentication handling:
Node.js
const express = require('express');
const { Analytics } = require('@customerio/cdp-analytics-node');
const app = express();
// Your actual Customer.io credentials (stored securely on backend)
const CIO_WRITE_KEY = process.env.CIO_WRITE_KEY;
// Create Customer.io client for forwarding events
const cioAnalytics = new Analytics({
writeKey: CIO_WRITE_KEY
});
// Extract authentication token from request
function extractClientToken(req) {
const authHeader = req.headers.authorization || '';
if (!authHeader.startsWith('Basic ')) {
return null;
}
// Decode the base64-encoded credentials
const base64Credentials = authHeader.split(' ')[1];
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
// Extract the token (the username part of user:password)
return credentials.split(':')[0];
}
// Validate the client's authentication token
function isValidClientToken(token) {
// Implement your authentication logic here
// This could check against a database, validate a JWT, etc.
return token && token.startsWith('client-api-key-');
}
Python
from flask import Flask, request, jsonify
import os
import base64
import json
from customerio_cdp_analytics import Analytics
app = Flask(__name__)
# Your actual Customer.io credentials (stored securely on backend)
CIO_WRITE_KEY = os.environ.get('CIO_WRITE_KEY')
# Create Customer.io client for forwarding events
cio_analytics = Analytics(write_key=CIO_WRITE_KEY)
# Extract authentication token from request
def extract_client_token(request):
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Basic '):
return None
# Decode the base64-encoded credentials
base64_credentials = auth_header.split(' ')[1]
credentials = base64.b64decode(base64_credentials).decode('utf-8')
# Extract the token (the username part of user:password)
return credentials.split(':')[0]
# Validate the client's authentication token
def is_valid_client_token(token):
# Implement your authentication logic here
# This could check against a database, validate a JWT, etc.
return token and token.startswith('client-api-key-')
Go
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"github.com/customerio/cdp-analytics-go"
)
var cioAnalytics *analytics.Client
func init() {
// Your actual Customer.io credentials (stored securely on backend)
cioWriteKey := os.Getenv("CIO_WRITE_KEY")
cioAnalytics = analytics.New(cioWriteKey)
}
// Extract authentication token from request
func extractClientToken(r *http.Request) string {
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Basic ") {
return ""
}
// Decode the base64-encoded credentials
base64Credentials := strings.TrimPrefix(authHeader, "Basic ")
credentials, err := base64.StdEncoding.DecodeString(base64Credentials)
if err != nil {
return ""
}
// Extract the token (the username part of user:password)
parts := strings.SplitN(string(credentials), ":", 2)
return parts[0]
}
// Validate the client's authentication token
func isValidClientToken(token string) bool {
// Implement your authentication logic here
// This could check against a database, validate a JWT, etc.
return strings.HasPrefix(token, "client-api-key-")
}
3. Create a function to forward events to Customer.io
You’ll need to capture requests so that you can forward them to Customer.io.
Node.js
async function forwardEventToCio(event) {
// Verify the event type is a valid method
if (typeof cioAnalytics[event.type] !== 'function') {
throw new Error(`Unknown event type: ${event.type}`);
}
// Forward the event to Customer.io
await cioAnalytics[event.type](event);
}
Python
def forward_event_to_cio(event):
event_type = event.get('type')
# Map event types to their handler methods
event_handlers = {
'identify': lambda: cio_analytics.identify(
user_id=event.get('userId'),
traits=event.get('traits', {}),
timestamp=event.get('timestamp'),
context=event.get('context')
),
'track': lambda: cio_analytics.track(
user_id=event.get('userId'),
event=event.get('event'),
properties=event.get('properties', {}),
timestamp=event.get('timestamp'),
context=event.get('context')
),
'page': lambda: cio_analytics.page(
user_id=event.get('userId'),
name=event.get('name'),
properties=event.get('properties', {}),
timestamp=event.get('timestamp'),
context=event.get('context')
),
'screen': lambda: cio_analytics.screen(
user_id=event.get('userId'),
name=event.get('name'),
properties=event.get('properties', {}),
timestamp=event.get('timestamp'),
context=event.get('context')
),
'group': lambda: cio_analytics.group(
user_id=event.get('userId'),
group_id=event.get('groupId'),
traits=event.get('traits', {}),
timestamp=event.get('timestamp'),
context=event.get('context')
)
}
handler = event_handlers.get(event_type)
if not handler:
raise ValueError(f"Unknown event type: {event_type}")
handler()
Go
func forwardEventToCio(event map[string]interface{}) error {
eventType, ok := event["type"].(string)
if !ok {
return fmt.Errorf("invalid event type")
}
// Map event types to their handler functions
eventHandlers := map[string]func() error{
"identify": func() error {
return cioAnalytics.Identify(&analytics.Identify{
UserId: event["userId"].(string),
Traits: event["traits"].(map[string]interface{}),
Timestamp: event["timestamp"],
Context: event["context"],
})
},
"track": func() error {
return cioAnalytics.Track(&analytics.Track{
UserId: event["userId"].(string),
Event: event["event"].(string),
Properties: event["properties"].(map[string]interface{}),
Timestamp: event["timestamp"],
Context: event["context"],
})
},
"page": func() error {
return cioAnalytics.Page(&analytics.Page{
UserId: event["userId"].(string),
Name: event["name"].(string),
Properties: event["properties"].(map[string]interface{}),
Timestamp: event["timestamp"],
Context: event["context"],
})
},
"screen": func() error {
return cioAnalytics.Screen(&analytics.Screen{
UserId: event["userId"].(string),
Name: event["name"].(string),
Properties: event["properties"].(map[string]interface{}),
Timestamp: event["timestamp"],
Context: event["context"],
})
},
"group": func() error {
return cioAnalytics.Group(&analytics.Group{
UserId: event["userId"].(string),
GroupId: event["groupId"].(string),
Traits: event["traits"].(map[string]interface{}),
Timestamp: event["timestamp"],
Context: event["context"],
})
},
}
handler, exists := eventHandlers[eventType]
if !exists {
return fmt.Errorf("unknown event type: %s", eventType)
}
return handler()
}
4. Handle incoming requests
Set up request handlers that tie everything together—extract credentials, validate requests, and forward requests to Customer.io.
Node.js
app.use((req, res) => {
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', async () => {
try {
// Extract and validate authentication token
const clientToken = extractClientToken(req);
if (!isValidClientToken(clientToken)) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Parse the request body
const payload = JSON.parse(body);
// Forward to Customer.io
if (req.url === '/v1/batch') {
// Forward batch events
for (const event of payload.batch) {
await forwardEventToCio(event);
}
} else {
// Handle other endpoints as needed
await forwardEventToCio(payload);
}
res.status(200).json({ success: true });
} catch (error) {
console.error('Error processing request:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Proxy server listening on port ${PORT}`);
});
Python
@app.route('/<path:path>', methods=['POST'])
def proxy_request(path):
try:
# Extract and validate authentication token
client_token = extract_client_token(request)
if not is_valid_client_token(client_token):
return jsonify({'error': 'Unauthorized'}), 401
# Parse the request body
payload = request.get_json()
# Forward to Customer.io
if path == 'v1/batch':
# Forward batch events
for event in payload.get('batch', []):
forward_event_to_cio(event)
else:
# Handle other endpoints as needed
forward_event_to_cio(payload)
return jsonify({'success': True}), 200
except Exception as e:
print(f'Error processing request: {e}')
return jsonify({'error': 'Internal server error'}), 500
if __name__ == '__main__':
port = int(os.environ.get('PORT', 3000))
app.run(host='0.0.0.0', port=port)
Go
func proxyHandler(w http.ResponseWriter, r *http.Request) {
// Extract and validate authentication token
clientToken := extractClientToken(r)
if !isValidClientToken(clientToken) {
http.Error(w, `{"error": "Unauthorized"}`, http.StatusUnauthorized)
return
}
// Read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, `{"error": "Invalid request"}`, http.StatusBadRequest)
return
}
// Parse the request body
var payload map[string]interface{}
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
return
}
// Forward to Customer.io
if strings.HasSuffix(r.URL.Path, "/v1/batch") {
// Forward batch events
batch := payload["batch"].([]interface{})
for _, e := range batch {
event := e.(map[string]interface{})
if err := forwardEventToCio(event); err != nil {
log.Printf("Error forwarding event: %v", err)
}
}
} else {
// Handle other endpoints as needed
if err := forwardEventToCio(payload); err != nil {
log.Printf("Error forwarding event: %v", err)
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"success": true}`))
}
func main() {
http.HandleFunc("/", proxyHandler)
port := os.Getenv("PORT")
if port == "" {
port = "3000"
}
log.Printf("Proxy server listening on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
Testing your proxy
To test your proxy implementation, configure a client SDK to send events to your proxy server instead of directly to Customer.io. Make sure you check the following things:
- Backend logs: Confirm the authentication token was extracted and validated correctly
- Event forwarding: Verify the request was successfully forwarded to Customer.io
- Customer.io workspace: Check that the event appears in your Customer.io data
- Error handling: Test with an invalid auth token to ensure unauthorized requests are rejected. See the Quick Start Guide for your SDK (JavaScript, Android, iOS, React Native, Flutter, Expo) for details on where to pass your auth token.
Troubleshooting
Events don’t appear in Customer.io
- Verify your backend receives requests from your client (check logs or the network tab in your browser).
- Confirm that the authentication token is being extracted and validated correctly by your backend. (Make sure you don’t get a
401 Unauthorizederror.) - Look for error responses from Customer.io’s API. (Make sure you don’t get
400 Bad Requesterrors.)
Authentication failures
- Verify the key you use in your client is the what your backend expects.
- Check that the
Authorizationheader is parsed correctly. - Confirm that your authentication logic works as expected.
