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

  1. Log in to BlackOps Center
  2. Go to Settings → Developer → API Keys
  3. Click "Create API Key"
  4. Name it (e.g., "Mobile App", "Zapier Integration")
  5. Copy the key immediately (only shown once)
  6. 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 item

Building 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_TOKEN

Integration 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

The best tools are the ones that play well with others. Build something cool? Share it with the community.

Building Custom Integrations - BlackOps Center