Blog
Architecture
Overview Harness Data Layer Skills
Pricing Get Started
Internal Reference

Credentials & Integrations

How secrets are managed, how OAuth tokens are acquired and stored, and what each channel module can actually do.

Bitwarden Secrets Manager

Single source of truth for all credentials. Machine token on the VPS. Secrets flow from Bitwarden → .env via sync script. OAuth-generated tokens write back to Bitwarden automatically.

How it's set up

# Token stored at (root read-only, 600 permissions):
/opt/cmo-analytics/.bws_token

# SDK: bitwarden_sdk (Python, installed in venv)
# Known secret IDs registry:
/opt/cmo-analytics/data/bws_known_ids.json

Sync script — three modes

scripts/bws_sync.py
# Pull all known secrets → .env (run this to refresh credentials)
python3 scripts/bws_sync.py

# Register a new secret added via Bitwarden dashboard
python3 scripts/bws_sync.py --add=<bitwarden-secret-uuid>

# Push a programmatically-generated credential → Bitwarden + .env
python3 scripts/bws_sync.py --push=LINKEDIN_ACCESS_TOKEN=eyJhbG...

Adding a new credential

1

Add in Bitwarden SM dashboard

vault.bitwarden.com → Secrets Manager → your project → New Secret. Name it exactly as you want it in .env (e.g., GOOGLE_ADS_TOKEN).

2

Copy the UUID

Each secret has a UUID. Copy it from the secret detail view.

3

Register + sync

python3 scripts/bws_sync.py --add=<uuid> — pulls value into .env immediately and registers the UUID so future syncs include it.


LinkedIn OAuth Flow

OAuth 2.0 with authorization code flow. Callback handled by a local server on port 3849, proxied by Caddy at meetkai.xyz/linkedin/callback. Token written to Bitwarden SM + .env on success.

One-time setup

1

Add redirect URI in LinkedIn app

LinkedIn Developer Portal → your app → Auth → OAuth 2.0 settings → add: https://meetkai.xyz/linkedin/callback

2

Start the callback server

python3 scripts/linkedin_oauth.py --start — starts server on 3849, prints the authorization URL.

3

Open URL in browser

Authorize the app. LinkedIn redirects to the callback. Token is exchanged and stored automatically.

4

Tokens stored

LINKEDIN_ACCESS_TOKEN, LINKEDIN_REFRESH_TOKEN, LINKEDIN_TOKEN_EXPIRES — written to both Bitwarden SM and .env.

Token refresh

# LinkedIn tokens expire in 60 days. Refresh before expiry:
python3 scripts/linkedin_oauth.py --refresh

# Check current token status:
cmo linkedin status

LinkedIn API endpoints used

scripts/linkedin.py
# API version pinned to 202501 (LinkedIn-Version header)
# All posts use /rest/posts (v2/shares is deprecated)

GET  /v2/userinfo                           ← profile + person URN
POST /rest/posts                            ← text post or article share
GET  /rest/posts?author=&q=author           ← recent posts
GET  /v2/networkSizes/urn:li:person:{id}    ← follower count
GET  /v2/organizationalEntityShareStatistics ← page analytics (needs Community Management API)

Current scopes

openid, profile, email        ← basic identity
w_member_social               ← post on personal profile ✅
r_basicprofile                ← read profile ✅

# Pending Community Management API approval:
r_organization_social         ← page analytics
w_organization_social         ← post to LinkedIn Pages

Channel Integration Status

LinkedIn
cmo linkedin
  • Credentials in Bitwarden
  • ⚠️ OAuth token needed
  • Post to profile (w_member_social)
  • Page analytics (pending approval)
Meta Ads
cmo meta_ads
  • Access token in .env
  • Ad account configured
  • Campaigns + spend
  • Impressions, CTR, CPL
Resend
cmo resend_report
  • API key in .env
  • 9 verified domains
  • Delivery status per send
  • Open/click rates (need webhooks)
Loops
cmo loops
  • 2 API keys (KaiCalls + default)
  • Add/find contacts
  • Trigger events
  • Campaign analytics (no API)
Instantly
cmo instantly
  • API key in .env
  • Campaign list + status
  • Sent/open/reply stats
  • 2 active campaigns
Google Ads
cmo google_ads
  • No developer token yet
  • No OAuth configured
  • Module not built
  • Build when running campaigns

Full command reference

# LinkedIn
cmo linkedin status                          # Token validity + expiry
cmo linkedin profile                         # Profile + URN
cmo linkedin post --text="..."              # Post to personal profile
cmo linkedin post-article --url=URL [--text="..."]
cmo linkedin recent                          # Last 10 posts
cmo linkedin followers                       # Follower count
cmo linkedin analytics --days=30             # Page stats (needs org scope)

# Meta Ads
cmo meta_ads campaigns                       # All campaigns + status
cmo meta_ads spend --days=7                  # Spend summary
cmo meta_ads performance --days=7            # Impressions, CTR, CPL, ROAS
cmo meta_ads dashboard                       # Full overview

# Resend
cmo resend_report domains                    # 9 verified domains + health
cmo resend_report recent --limit=20          # Sent emails + delivery status
cmo resend_report stats --limit=100          # By-site delivery breakdown

# Loops
cmo loops dashboard                          # Account status
cmo loops find [email protected]      # Find contact
cmo loops add --email=x --first_name=Y       # Add/update contact
cmo loops event --email=x --event=name       # Trigger automation
cmo loops send --email=x --transaction_id=id # Send transactional email

# Instantly
cmo instantly campaigns                      # List campaigns
cmo instantly stats --all                    # Stats across all campaigns
cmo instantly stats --campaign_id=ID         # Single campaign stats

File Layout

/opt/cmo-analytics/
├── .bws_token              # Bitwarden SM machine token (600, root only)
├── .env                    # All credentials — synced from Bitwarden
├── data/
│   └── bws_known_ids.json  # {uuid: key_name} registry for sync
└── scripts/
    ├── bws_sync.py         # Bitwarden SM ↔ .env sync (pull + push)
    ├── linkedin_oauth.py   # OAuth flow + token refresh
    ├── linkedin.py         # LinkedIn CLI module
    ├── meta_ads.py         # Meta Ads CLI module
    ├── resend_report.py    # Resend delivery reporting
    ├── loops.py            # Loops contact + event management
    └── instantly.py        # Instantly cold email analytics

/etc/caddy/Caddyfile
└── meetkai.xyz /linkedin/callback → 127.0.0.1:3849  # OAuth callback