Social Media Credentials Guide
This document covers the credentials required for automated social media publishing and how to securely store them.
Overview
| Platform | Auth Method | Required Secrets |
|---|---|---|
| OAuth 2.0 | LINKEDIN_ACCESS_TOKEN, LINKEDIN_ORG_ID |
|
| X/Twitter | OAuth 2.0 | TWITTER_API_KEY, TWITTER_API_SECRET, TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_SECRET |
| Bluesky | App Password | BLUESKY_HANDLE, BLUESKY_APP_PASSWORD |
| Mastodon | OAuth 2.0 | MASTODON_INSTANCE, MASTODON_ACCESS_TOKEN |
| Buffer | API Key | BUFFER_ACCESS_TOKEN |
Credentials Required
| Secret Name | Description | Example |
|---|---|---|
LINKEDIN_ACCESS_TOKEN |
OAuth 2.0 access token | AQV...xyz |
LINKEDIN_ORG_ID |
Organization/Company ID (for company posts) | 12345678 |
LINKEDIN_PERSON_URN |
Personal URN (for personal posts) | urn:li:person:abc123 |
How to Obtain
- Create LinkedIn App:
- Go to LinkedIn Developers
- Create a new app under your company page
- Request access to:
w_member_social,r_liteprofile,r_organization_social
- Get OAuth Token:
# OAuth 2.0 Authorization URL https://www.linkedin.com/oauth/v2/authorization? response_type=code& client_id=YOUR_CLIENT_ID& redirect_uri=YOUR_REDIRECT_URI& scope=w_member_social%20r_liteprofile # Exchange code for token curl -X POST https://www.linkedin.com/oauth/v2/accessToken \ -d "grant_type=authorization_code" \ -d "code=YOUR_AUTH_CODE" \ -d "redirect_uri=YOUR_REDIRECT_URI" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" - Find Organization ID:
- Go to your Company Page on LinkedIn
- URL format:
linkedin.com/company/12345678 - The number is your Organization ID
Token Refresh
LinkedIn tokens expire after 60 days. Set up a refresh workflow:
# .github/workflows/refresh-linkedin-token.yml
name: Refresh LinkedIn Token
on:
schedule:
- cron: '0 0 1,15 * *' # 1st and 15th of each month
jobs:
refresh:
runs-on: ubuntu-latest
steps:
- name: Refresh Token
run: |
# Implement token refresh logic
# Store new token in GitHub Secrets via API
API Usage
# Post to personal profile
curl -X POST https://api.linkedin.com/v2/ugcPosts \
-H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"author": "urn:li:person:YOUR_PERSON_URN",
"lifecycleState": "PUBLISHED",
"specificContent": {
"com.linkedin.ugc.ShareContent": {
"shareCommentary": { "text": "Your post content" },
"shareMediaCategory": "NONE"
}
},
"visibility": { "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" }
}'
X/Twitter
Credentials Required
| Secret Name | Description |
|---|---|
TWITTER_API_KEY |
API Key (Consumer Key) |
TWITTER_API_SECRET |
API Secret (Consumer Secret) |
TWITTER_ACCESS_TOKEN |
User Access Token |
TWITTER_ACCESS_SECRET |
User Access Token Secret |
TWITTER_BEARER_TOKEN |
Bearer Token (for read-only) |
How to Obtain
- Apply for Developer Account:
- Go to Twitter Developer Portal
- Apply for Elevated access (required for posting)
- Create Project & App:
- Create a new Project
- Create an App within the Project
- Set App permissions to “Read and Write”
- Generate Tokens:
- In App settings → Keys and Tokens
- Generate API Key and Secret
- Generate Access Token and Secret
- Copy Bearer Token
Rate Limits
| Endpoint | Limit |
|---|---|
| POST tweets | 200/15 min (user), 300/15 min (app) |
| Media upload | 615 uploads/15 min |
API Usage (v2)
# Post a tweet
curl -X POST "https://api.twitter.com/2/tweets" \
-H "Authorization: Bearer $TWITTER_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "Your tweet content"}'
Bluesky
Credentials Required
| Secret Name | Description | Example |
|---|---|---|
BLUESKY_HANDLE |
Your Bluesky handle | yourname.bsky.social |
BLUESKY_APP_PASSWORD |
App-specific password | xxxx-xxxx-xxxx-xxxx |
How to Obtain
- Log into Bluesky:
- Go to Settings → App Passwords
- Click “Add App Password”
- Name it (e.g., “GitHub Actions”)
- Copy the generated password
- Note: App passwords are safer than your main password and can be revoked individually.
API Usage
# Create session
SESSION=$(curl -s -X POST https://bsky.social/xrpc/com.atproto.server.createSession \
-H "Content-Type: application/json" \
-d "{\"identifier\": \"$BLUESKY_HANDLE\", \"password\": \"$BLUESKY_APP_PASSWORD\"}")
ACCESS_JWT=$(echo $SESSION | jq -r '.accessJwt')
DID=$(echo $SESSION | jq -r '.did')
# Create post
curl -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \
-H "Authorization: Bearer $ACCESS_JWT" \
-H "Content-Type: application/json" \
-d "{
\"repo\": \"$DID\",
\"collection\": \"app.bsky.feed.post\",
\"record\": {
\"\$type\": \"app.bsky.feed.post\",
\"text\": \"Your post content\",
\"createdAt\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
}
}"
Mastodon
Credentials Required
| Secret Name | Description | Example |
|---|---|---|
MASTODON_INSTANCE |
Your Mastodon instance URL | https://mastodon.social |
MASTODON_ACCESS_TOKEN |
OAuth access token | abc123... |
How to Obtain
- Create Application:
- Go to Preferences → Development → New Application
- Name: “GitHub Actions Publisher”
- Scopes:
read,write,write:statuses - Save
- Get Access Token:
- After creating, click your app name
- Copy “Your access token”
API Usage
# Post a status
curl -X POST "$MASTODON_INSTANCE/api/v1/statuses" \
-H "Authorization: Bearer $MASTODON_ACCESS_TOKEN" \
-F "status=Your post content" \
-F "visibility=public"
Buffer (Aggregator Option)
Buffer allows posting to multiple platforms from a single API.
Credentials Required
| Secret Name | Description |
|---|---|
BUFFER_ACCESS_TOKEN |
OAuth access token |
How to Obtain
- Create Buffer Account: buffer.com
- Connect Social Accounts: Link LinkedIn, X, etc.
- Create Access Token:
- Go to Buffer Developers
- Create an app
- Get access token
API Usage
# Create a post
curl -X POST "https://api.bufferapp.com/1/updates/create.json" \
-d "access_token=$BUFFER_ACCESS_TOKEN" \
-d "profile_ids[]=YOUR_PROFILE_ID" \
-d "text=Your post content" \
-d "scheduled_at=$(date -d '+1 hour' +%s)"
Storing Secrets in GitHub
Repository Secrets
- Go to Repository → Settings → Secrets and variables → Actions
- Click “New repository secret”
- Add each credential with its name and value
Repository Settings
└── Secrets and variables
└── Actions
├── LINKEDIN_ACCESS_TOKEN
├── LINKEDIN_ORG_ID
├── TWITTER_API_KEY
├── TWITTER_API_SECRET
├── TWITTER_ACCESS_TOKEN
├── TWITTER_ACCESS_SECRET
├── BLUESKY_HANDLE
├── BLUESKY_APP_PASSWORD
├── MASTODON_INSTANCE
└── MASTODON_ACCESS_TOKEN
Environment Secrets (Recommended)
For better security, use environments with protection rules:
- Go to Settings → Environments → New environment
- Create
productionenvironment - Add secrets to environment (not repository)
- Enable required reviewers for production deployments
# Workflow using environment
jobs:
publish:
runs-on: ubuntu-latest
environment: production # Requires approval
steps:
- name: Post to LinkedIn
env:
LINKEDIN_TOKEN: $
run: |
# Use the secret
Organization Secrets
For multi-repo use, store at organization level:
- Go to Organization → Settings → Secrets and variables → Actions
- Add secrets with repository access policies
Security Best Practices
Do
- Use environment protection rules for production secrets
- Rotate tokens regularly (set calendar reminders)
- Use app-specific passwords where available (Bluesky)
- Limit token scopes to minimum required
- Monitor API usage for anomalies
- Use GitHub’s secret scanning alerts
Don’t
- Commit secrets to repository (even in
.envfiles) - Share tokens across multiple applications
- Use personal passwords in automation
- Log token values in workflow output
- Store secrets in plain text files
Token Rotation Schedule
| Platform | Token Lifetime | Rotation Frequency |
|---|---|---|
| 60 days | Every 45 days | |
| X/Twitter | Never expires | Every 90 days (recommended) |
| Bluesky | Never expires | Every 90 days (recommended) |
| Mastodon | Never expires | Every 90 days (recommended) |
Workflow Integration Example
# .github/workflows/publish-social.yml
name: Publish Social Posts
on:
workflow_dispatch:
inputs:
platform:
description: 'Platform to publish to'
required: true
type: choice
options:
- linkedin
- twitter
- bluesky
- mastodon
- all
jobs:
publish:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Publish to LinkedIn
if: inputs.platform == 'linkedin' || inputs.platform == 'all'
env:
LINKEDIN_ACCESS_TOKEN: $
LINKEDIN_ORG_ID: $
run: |
# LinkedIn publishing logic
- name: Publish to X/Twitter
if: inputs.platform == 'twitter' || inputs.platform == 'all'
env:
TWITTER_API_KEY: $
TWITTER_API_SECRET: $
TWITTER_ACCESS_TOKEN: $
TWITTER_ACCESS_SECRET: $
run: |
# Twitter publishing logic
- name: Publish to Bluesky
if: inputs.platform == 'bluesky' || inputs.platform == 'all'
env:
BLUESKY_HANDLE: $
BLUESKY_APP_PASSWORD: $
run: |
# Bluesky publishing logic
- name: Publish to Mastodon
if: inputs.platform == 'mastodon' || inputs.platform == 'all'
env:
MASTODON_INSTANCE: $
MASTODON_ACCESS_TOKEN: $
run: |
# Mastodon publishing logic
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| 401 Unauthorized | Expired token | Refresh or regenerate token |
| 403 Forbidden | Insufficient permissions | Check app scopes |
| 429 Too Many Requests | Rate limit exceeded | Add delays between posts |
| Token not found | Secret name mismatch | Verify secret names match workflow |
Debugging Tips
# Add debug output (remove in production)
- name: Debug
run: |
echo "Token length: ${#LINKEDIN_ACCESS_TOKEN}"
# Never echo the actual token!
Related Documentation
- AUTOMATION.md - GitHub Actions workflow reference
- WORKFLOW.md - Content publishing workflow
- SCHEDULING.md - Post scheduling options