Proxying requests to Customer.io

Updated

Proxying 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:

  1. Configure your client (mobile app, website, etc.) to send requests to your backend instead of Customer.io.
  2. Set up your backend to receive requests, extract authentication tokens, and validate them.
  3. Forward events to Customer.io by parsing events and sending them with your actual Customer.io credentials.
  4. 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.

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:

  1. Initialize a server and Customer.io SDK client with your actual credentials (stored securely)
  2. Extract the authentication token from incoming requests
  3. Validate the token against your authentication system
  4. Prepare a valid credential for the requests you forward to Customer.io

Here’s a complete implementation showing server setup and authentication handling:

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-');
}
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-')
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.

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);
}
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()
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.

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}`);
});
@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)
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 Unauthorized error.)
  • Look for error responses from Customer.io’s API. (Make sure you don’t get 400 Bad Request errors.)

Authentication failures

  • Verify the key you use in your client is the what your backend expects.
  • Check that the Authorization header is parsed correctly.
  • Confirm that your authentication logic works as expected.
Copied to clipboard!
  Contents