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.