Guide: Building Custom Integrations

Guide to building custom integrations with the Internal Newsletter API. Authentication, error handling, pagination, and code examples in Python, JavaScript, and Ruby.

Everything you need to build a robust integration with the Internal Newsletter API.

API client setup

Python

import requests

class NewsletterClient:
    def __init__(self, base_url, api_key):
        self.base_url = f"{base_url}/api/v1"
        self.headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }

    def get(self, path, params=None):
        r = requests.get(f"{self.base_url}{path}", headers=self.headers, params=params)
        r.raise_for_status()
        return r.json()

    def post(self, path, data):
        r = requests.post(f"{self.base_url}{path}", headers=self.headers, json=data)
        r.raise_for_status()
        return r.json()

    def patch(self, path, data):
        r = requests.patch(f"{self.base_url}{path}", headers=self.headers, json=data)
        r.raise_for_status()
        return r.json()

    def delete(self, path):
        r = requests.delete(f"{self.base_url}{path}", headers=self.headers)
        r.raise_for_status()

client = NewsletterClient("https://your-app.com", "inl_your_key")

JavaScript / Node.js

class NewsletterClient {
  constructor(baseUrl, apiKey) {
    this.baseUrl = `${baseUrl}/api/v1`;
    this.headers = {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    };
  }

  async request(method, path, body = null) {
    const options = { method, headers: this.headers };
    if (body) options.body = JSON.stringify(body);
    const res = await fetch(`${this.baseUrl}${path}`, options);
    if (!res.ok) throw new Error(`API error: ${res.status}`);
    return res.status === 204 ? null : res.json();
  }

  get(path) { return this.request('GET', path); }
  post(path, body) { return this.request('POST', path, body); }
  patch(path, body) { return this.request('PATCH', path, body); }
  delete(path) { return this.request('DELETE', path); }
}

Ruby

require "net/http"
require "json"

class NewsletterClient
  def initialize(base_url, api_key)
    @base_uri = URI("#{base_url}/api/v1")
    @headers = {
      "Authorization" => "Bearer #{api_key}",
      "Content-Type" => "application/json"
    }
  end

  def get(path)
    request(Net::HTTP::Get, path)
  end

  def post(path, body)
    request(Net::HTTP::Post, path, body)
  end

  private

  def request(method_class, path, body = nil)
    uri = URI("#{@base_uri}#{path}")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme == "https"
    req = method_class.new(uri, @headers)
    req.body = body.to_json if body
    res = http.request(req)
    JSON.parse(res.body) if res.body.present?
  end
end

Error handling

Always check for these error cases:

Status What to do
401 API key is invalid, expired, or revoked. Check your key.
403 Key doesn't have the required scope. Regenerate with correct scopes.
404 Resource doesn't exist or belongs to another team. Check the ID.
422 Validation failed. Check the errors array for details.
429 Rate limited or plan limit reached. Wait and retry, or upgrade.

Pagination

For list endpoints, always handle pagination:

def get_all_blocks(client, status="ready"):
    all_blocks = []
    page = 1
    while True:
        data = client.get(f"/content_blocks?status={status}&page={page}&per_page=100")
        all_blocks.extend(data["content_blocks"])
        if page >= data["meta"]["total_pages"]:
            break
        page += 1
    return all_blocks

Testing

Use a separate API key for testing. Create one with a descriptive name like "Test Integration" so you can identify and revoke it easily.

All resources created via the API are real — there's no sandbox environment. Create a test team if you need isolation.