Skip to main content

Configuring the Litmus API in GitHub Actions for Automated Email QA

Integrating Litmus API into GitHub Actions eliminates manual cross-client rendering checks and enforces strict visual standards in modern transactional pipelines. This guide provides the exact authentication headers, payload structures, and CI/CD workflow syntax required to trigger automated Litmus tests on every pull request. Before implementing the configuration, align this setup with established Email Testing & QA Workflows to maintain deliverability baselines and prevent regression.

Prerequisites and API Authentication Setup

  1. Enable API Access: Log into your Litmus dashboard. Navigate to Account Settings > API Access and generate a Personal Access Token (PAT).
  2. Secure Storage: Add the token to GitHub: Repository Settings > Secrets and variables > Actions > New repository secret.
  • Name: LITMUS_API_KEY
  • Value: [your_litmus_pat]
  1. Auth Standard: The Litmus REST API requires Authorization: Bearer <token> for all endpoints. Never hardcode credentials or commit tokens to version control. GitHub Actions injects secrets at runtime and masks them in runner logs.

Constructing the GitHub Actions Workflow YAML

Create .github/workflows/email-validation.yml at the repository root. The configuration below compiles your email template, authenticates securely, and posts the payload to the Litmus /v3/tests endpoint.

name: Email Validation Pipeline
on:
 push:
 branches: [main]
 pull_request:
 branches: [develop]

permissions:
 contents: read
 checks: write

jobs:
 litmus-qa:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v4
 - uses: actions/setup-node@v4
 with:
 node-version: 20
 cache: npm

 # Replace with your actual template compilation command
 - name: Build Email HTML
 run: |
 npm ci
 npm run build:email
 # Read compiled HTML into environment variable
 echo "HTML_PAYLOAD=$(cat dist/email.html | tr -d '\n' | sed 's/"/\\"/g')" >> $GITHUB_ENV

 - name: Trigger Litmus API Test
 id: trigger
 run: |
 set -e
 RESPONSE=$(curl -s -w "\n%{http_code}" -X POST https://api.litmus.com/v3/tests \
 -H "Authorization: Bearer ${{ secrets.LITMUS_API_KEY }}" \
 -H "Content-Type: application/json" \
 -d "{
 \"test\": {
 \"subject\": \"Automated QA Check\",
 \"from_email_address\": \"qa@yourdomain.com\",
 \"email_client_ids\": [1, 2, 3, 4, 5],
 \"html_source\": \"${{ env.HTML_PAYLOAD }}\"
 }
 }")
 
 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1)
 BODY=$(echo "$RESPONSE" | sed '$d')
 
 if [ "$HTTP_CODE" -ne 201 ]; then
 echo "::error::Litmus API rejected request. HTTP $HTTP_CODE"
 echo "$BODY"
 exit 1
 fi
 
 TEST_ID=$(echo "$BODY" | jq -r '.id')
 echo "test_id=$TEST_ID" >> $GITHUB_OUTPUT

Handling Asynchronous Webhook Callbacks

Litmus processes cross-client rendering asynchronously. Implement a polling loop with exponential backoff to query /v3/tests/{id} until completion. Parse the rendering_results array and fail the GitHub check if critical parsing errors are detected.

 - name: Poll Rendering Status
 run: |
 MAX_RETRIES=25
 RETRY_COUNT=0
 DELAY=10
 
 while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
 STATUS_RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.LITMUS_API_KEY }}" \
 "https://api.litmus.com/v3/tests/${{ steps.trigger.outputs.test_id }}")
 
 STATUS=$(echo "$STATUS_RESPONSE" | jq -r '.status')
 
 if [ "$STATUS" = "completed" ]; then
 echo "Rendering complete."
 break
 elif [ "$STATUS" = "failed" ]; then
 echo "::error::Litmus processing failed. Check payload validity."
 exit 1
 fi
 
 echo "Status: $STATUS. Waiting ${DELAY}s before retry..."
 sleep $DELAY
 DELAY=$((DELAY * 2))
 RETRY_COUNT=$((RETRY_COUNT + 1))
 done
 
 if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
 echo "::error::Polling timeout reached. Verify API health."
 exit 1
 fi

 - name: Validate Rendering Results
 run: |
 RESULTS=$(curl -s -H "Authorization: Bearer ${{ secrets.LITMUS_API_KEY }}" \
 "https://api.litmus.com/v3/tests/${{ steps.trigger.outputs.test_id }}/results")
 
 # Fail pipeline if critical rendering errors exceed threshold
 CRITICAL_ERRORS=$(echo "$RESULTS" | jq '[.rendering_results[] | select(.status == "error")] | length')
 if [ "$CRITICAL_ERRORS" -gt 0 ]; then
 echo "::error::Found $CRITICAL_ERRORS critical rendering errors. Review Litmus dashboard."
 exit 1
 fi
 echo "All rendering checks passed."

This automated gatekeeping is a cornerstone of robust Litmus & Email on Acid Workflows and ensures broken templates never reach production environments.

Troubleshooting Common Configuration Errors

HTTP Status Root Cause Resolution
401 Unauthorized Expired PAT, incorrect scope, or IP allowlist blocking runner IPs. Regenerate token. Verify LITMUS_API_KEY secret length matches dashboard. Disable IP allowlisting for GitHub runner ranges if applicable.
400 Bad Request Malformed JSON, missing email_client_ids, or HTML payload > 5MB. Validate payload with jq . before POST. Strip inline styles exceeding size limits. Ensure Content-Type: application/json is explicitly set.
429 Too Many Requests Exceeding Litmus API rate limits during parallel PR checks. Implement exponential backoff (as configured). Serialize workflow runs using concurrency: group: email-qa-${{ github.ref }} in YAML.
jq: command not found Missing JSON parser in runner environment. Add sudo apt-get install -y jq to workflow steps or use actions/github-script with Node.js JSON.parse().

Debugging Protocol:

  1. Set repository variable ACTIONS_RUNNER_DEBUG=true to enable verbose runner logs.
  2. Add -v to curl commands to inspect exact request/response headers.
  3. Verify workflow permissions: permissions: checks: write is mandatory for GitHub Check API integration.

Production Testing & Validation Steps

  1. Dry Run Locally: Use act or GitHub CLI gh workflow run email-validation.yml --ref <branch> to trigger without merging.
  2. Secret Masking Verification: Add a debug step echo "::add-mask::${{ secrets.LITMUS_API_KEY }}" to confirm GitHub masks the token in logs.
  3. Failure Path Testing: Intentionally inject malformed HTML (e.g., unclosed <table> tags) into your template. Verify the workflow exits with code 1 and posts a failed check status to the PR.
  4. Success Validation: Merge a clean PR. Confirm the GitHub Checks tab shows Email Validation Pipeline as green and links to the Litmus test report.

Integrating Litmus API into GitHub Actions standardizes visual QA across distributed teams. By enforcing strict payload validation, implementing exponential backoff, and gating merges on rendering results, you eliminate manual review overhead and guarantee production-ready email templates.