Building Custom Integrations
BlackOps Center is built to be extended. Whether you're connecting to your CRM, building a mobile app, or automating workflows with Zapier, our API and webhook system make it possible.
Integration Approaches
1. REST API
Use Case: Pull data from BlackOps Center or push data to it programmatically
Examples:
- Fetch blog posts for a mobile app
- Create reservoir items from external tools
- Sync content to a CMS
- Build custom analytics dashboards
Authentication: API keys (bearer token)
2. Webhooks
Use Case: React to events in real-time without polling
Examples:
- Notify Slack when content is published
- Update CRM when user subscribes
- Trigger CI/CD pipeline on content changes
- Sync analytics to data warehouse
Authentication: Signature verification
3. Email Integration
Use Case: Forward content to BlackOps Center via email
Examples:
- Forward newsletters to reservoirs
- Save Gmail starred emails as content
- Email voice memos (via transcription service)
Authentication: Unique reservoir email addresses
4. Browser Extension
Use Case: Capture content while browsing
Examples:
- One-click bookmark to reservoir
- Right-click to save selection
- Automatic capture of starred tweets
Authentication: OAuth or API key
REST API
Base URL
https://blackopscenter.com/api
Authentication
All API requests require an API key passed as a bearer token:
Authorization: Bearer YOUR_API_KEY
Generating API Keys
- Log in to BlackOps Center
- Go to Settings → Developer → API Keys
- Click "Create API Key"
- Name it (e.g., "Mobile App", "Zapier Integration")
- Copy the key immediately (only shown once)
- Store securely
🔒 API Key Security
- Never commit keys to version control
- Use environment variables
- Rotate keys periodically
- Revoke keys when no longer needed
- Use separate keys per integration (easier to revoke)
Common API Endpoints
Reservoirs
# List reservoirs
GET /api/admin/reservoirs?site_id={siteId}
# Get reservoir details
GET /api/admin/reservoirs/{reservoirId}
# Create reservoir
POST /api/admin/reservoirs
{
"name": "React Performance",
"description": "Performance tips and patterns",
"site_id": "xxx"
}
# Add item to reservoir
POST /api/admin/reservoirs/{reservoirId}/items
{
"title": "React 19 useMemo Changes",
"content": "Full content here...",
"url": "https://example.com/post",
"tags": ["react", "performance"]
}Blog Posts
# List published posts
GET /api/blog/posts?site_id={siteId}&limit=10
# Get single post
GET /api/blog/posts/{slug}
# Create draft
POST /api/admin/posts
{
"title": "Getting Started with Next.js",
"content": "Markdown content...",
"status": "draft",
"site_id": "xxx"
}
# Publish post
PATCH /api/admin/posts/{postId}
{
"status": "published",
"published_at": "2025-02-01T10:00:00Z"
}Newsletter Subscribers
# List subscribers
GET /api/admin/newsletters/{newsletterId}/subscribers
# Add subscriber
POST /api/admin/newsletters/{newsletterId}/subscribe
{
"email": "user@example.com",
"name": "User Name"
}
# Unsubscribe
POST /api/admin/newsletters/{newsletterId}/unsubscribe
{
"email": "user@example.com"
}Analytics
# Get site analytics
GET /api/admin/analytics?site_id={siteId}&start_date=2025-01-01&end_date=2025-01-31
# Track custom event
POST /api/track/event
{
"event_name": "custom_cta_click",
"site_id": "xxx",
"properties": {
"cta_location": "sidebar",
"post_slug": "react-performance"
}
}Error Handling
API returns standard HTTP status codes:
200 OK - Success
201 Created - Resource created
400 Bad Request - Invalid input
401 Unauthorized - Missing/invalid API key
403 Forbidden - No access to resource
404 Not Found - Resource doesn't exist
429 Too Many Requests - Rate limit exceeded
500 Internal Server Error - Server error
# Error response format
{
"error": "Invalid API key",
"code": "INVALID_AUTH",
"details": {
"message": "The API key provided is not valid"
}
}Rate Limiting
API requests are rate-limited per API key:
- Free Tier: 100 requests/hour
- Pro Tier: 1,000 requests/hour
- Enterprise Tier: 10,000 requests/hour
Rate limit headers:
X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 847 X-RateLimit-Reset: 1738368000
Integration Examples
Example 1: Sync Notion Pages to Reservoir
// notion-to-blackops.js
import { Client } from '@notionhq/client'
const notion = new Client({ auth: process.env.NOTION_API_KEY })
const BLACKOPS_API_KEY = process.env.BLACKOPS_API_KEY
const RESERVOIR_ID = 'your-reservoir-id'
async function syncNotionToReservoir() {
// Fetch pages from Notion database
const response = await notion.databases.query({
database_id: process.env.NOTION_DATABASE_ID,
filter: {
property: 'Status',
select: { equals: 'Ready to Sync' }
}
})
for (const page of response.results) {
// Extract content
const title = page.properties.Name.title[0]?.plain_text
const content = await getPageContent(page.id)
// Create reservoir item
await fetch(`https://blackopscenter.com/api/admin/reservoirs/${RESERVOIR_ID}/items`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${BLACKOPS_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title,
content,
source_type: 'notion',
source_id: page.id,
tags: ['notion', 'synced']
})
})
// Update Notion status
await notion.pages.update({
page_id: page.id,
properties: {
Status: { select: { name: 'Synced' } }
}
})
}
}
// Run daily
setInterval(syncNotionToReservoir, 24 * 60 * 60 * 1000)Example 2: Slack Notification on Post Publish
// webhook-handler.js (your server)
import { WebhookClient } from '@slack/webhook'
const slackWebhook = new WebhookClient(process.env.SLACK_WEBHOOK_URL)
export async function POST(request) {
const event = await request.json()
if (event.type === 'post.published') {
const post = event.data
await slackWebhook.send({
text: `📝 New blog post published!`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*${post.title}*\n${post.excerpt}`
}
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'View Post' },
url: post.url
}
]
}
]
})
}
return new Response('OK', { status: 200 })
}Example 3: iOS Shortcut to Add Reservoir Item
# iOS Shortcuts app (pseudo-code)
1. Get URL from Share Sheet
2. Extract metadata (title, image) via web scraping
3. Get text input "Add notes (optional)"
4. Make API request:
POST https://blackopscenter.com/api/admin/reservoirs/{reservoirId}/items
Headers:
Authorization: Bearer {API_KEY}
Body:
{
"url": {URL from Share Sheet},
"title": {Extracted Title},
"notes": {User Notes},
"source_type": "mobile_shortcut",
"tags": ["mobile-saved"]
}
5. Show notification "Saved to reservoir"Example 4: Zapier Integration
# Zapier Workflow
Trigger: New row in Google Sheets
Action: HTTP POST to BlackOps Center API
Setup:
1. Trigger: New or Updated Row in Google Sheets
- Spreadsheet: Content Ideas
- Worksheet: Ideas
2. Action: Webhooks by Zapier - POST
- URL: https://blackopscenter.com/api/admin/reservoirs/xxx/items
- Headers:
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
- Data:
{
"title": {{Title}},
"content": {{Description}},
"tags": ["google-sheets", "idea"],
"source_type": "zapier"
}
Result: Google Sheets → Automatic reservoir itemBuilding a Chrome Extension
Manifest v3 Example
// manifest.json
{
"manifest_version": 3,
"name": "BlackOps Center Clipper",
"version": "1.0",
"permissions": [
"activeTab",
"storage"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_icon": "icon.png"
},
"commands": {
"save-to-reservoir": {
"suggested_key": {
"default": "Ctrl+Shift+S"
},
"description": "Save to reservoir"
}
}
}Background Script
// background.js
chrome.commands.onCommand.addListener((command) => {
if (command === 'save-to-reservoir') {
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
const tab = tabs[0]
// Get API key from storage
const { apiKey } = await chrome.storage.sync.get('apiKey')
// Save current page
await fetch('https://blackopscenter.com/api/admin/reservoirs/xxx/items', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: tab.title,
url: tab.url,
source_type: 'chrome_extension',
tags: ['chrome-saved']
})
})
// Show notification
chrome.notifications.create({
type: 'basic',
iconUrl: 'icon.png',
title: 'Saved to Reservoir',
message: tab.title
})
})
}
})Best Practices
1. Handle Errors Gracefully
async function createReservoirItem(data) {
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
if (!response.ok) {
const error = await response.json()
throw new Error(`API Error: ${error.message}`)
}
return await response.json()
} catch (error) {
console.error('Failed to create item:', error)
// Don't crash - queue for retry or notify user
await queueForRetry(data)
}
}2. Implement Retry Logic
async function apiRequestWithRetry(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options)
if (response.ok) return response
// Retry on 5xx errors
if (response.status >= 500) {
await sleep(Math.pow(2, i) * 1000) // Exponential backoff
continue
}
// Don't retry on 4xx errors
throw new Error(`HTTP ${response.status}`)
} catch (error) {
if (i === maxRetries - 1) throw error
await sleep(Math.pow(2, i) * 1000)
}
}
}3. Respect Rate Limits
class RateLimitedClient {
constructor(apiKey, requestsPerHour = 1000) {
this.apiKey = apiKey
this.requestsPerHour = requestsPerHour
this.requests = []
}
async request(url, options) {
// Remove requests older than 1 hour
const oneHourAgo = Date.now() - 3600000
this.requests = this.requests.filter(t => t > oneHourAgo)
// Check rate limit
if (this.requests.length >= this.requestsPerHour) {
const oldestRequest = this.requests[0]
const waitTime = oldestRequest + 3600000 - Date.now()
throw new Error(`Rate limit exceeded. Wait ${waitTime}ms`)
}
// Make request
this.requests.push(Date.now())
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.apiKey}`
}
})
}
}4. Cache API Responses
const cache = new Map()
async function getCachedReservoirs(siteId) {
const cacheKey = `reservoirs-${siteId}`
// Check cache
if (cache.has(cacheKey)) {
const { data, timestamp } = cache.get(cacheKey)
// Cache valid for 5 minutes
if (Date.now() - timestamp < 300000) {
return data
}
}
// Fetch fresh data
const response = await fetch(`/api/admin/reservoirs?site_id=${siteId}`)
const data = await response.json()
// Update cache
cache.set(cacheKey, {
data,
timestamp: Date.now()
})
return data
}5. Validate Input
import { z } from 'zod'
const ReservoirItemSchema = z.object({
title: z.string().min(1).max(255),
content: z.string().optional(),
url: z.string().url().optional(),
tags: z.array(z.string()).max(10),
source_type: z.string().optional()
})
// Validate before sending
const data = {
title: userInput.title,
content: userInput.content,
url: userInput.url,
tags: userInput.tags
}
try {
const validated = ReservoirItemSchema.parse(data)
await createReservoirItem(validated)
} catch (error) {
console.error('Validation error:', error.errors)
// Show user-friendly error
}OAuth Integration (Coming Soon)
OAuth 2.0 support is coming soon for third-party applications. This will enable:
- User authorization without sharing API keys
- Scoped permissions (read-only, write-only, etc.)
- Token refresh and expiration
- Revocation by users
OAuth flow will look like:
1. Redirect user to:
https://blackopscenter.com/oauth/authorize?
client_id=YOUR_CLIENT_ID&
redirect_uri=https://yourapp.com/callback&
scope=reservoirs:write,posts:read
2. User approves access
3. BlackOps Center redirects back:
https://yourapp.com/callback?code=AUTHORIZATION_CODE
4. Exchange code for access token:
POST https://blackopscenter.com/oauth/token
{
"code": "AUTHORIZATION_CODE",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"grant_type": "authorization_code"
}
5. Use access token for API requests:
Authorization: Bearer ACCESS_TOKENIntegration Gallery
Community-built integrations (coming soon):
- Notion Sync: Bi-directional sync between Notion and reservoirs
- Airtable Bridge: Use Airtable as content source
- Make (Integromat): Visual workflow automation
- n8n: Self-hosted workflow automation
- Obsidian Plugin: Save notes to reservoirs
- Readwise: Import highlights to reservoirs
Getting Help
- API Reference: Full endpoint documentation
- Webhooks Guide: Webhook configuration
- GitHub Examples: Sample integrations
- Discord Community: Ask questions, share integrations
The best tools are the ones that play well with others. Build something cool? Share it with the community.