BLOG // [DEVOPS] Bash and REST API: Calling and Handling APIs Effectively in DevOps
Bash and REST API: Calling and Handling APIs Effectively in DevOps

Bash and REST API: Calling and Handling APIs Effectively in DevOps

Comprehensive guide to calling REST APIs with curl in Bash: GET/POST/PUT/DELETE, JSON parsing with jq, bearer auth, retry logic, and automating GitHub/Slack APIs in DevOps.

DEVOPS BASH

Bash and REST API in DevOps

In the previous post, we explored optimizing performance with Parallel Execution. Now, we combine Bash with REST APIs—the bridge to communicating with nearly every modern service: from GitHub, Slack, and Jira, to monitoring dashboards and CI/CD platforms.

In DevOps, APIs are the way to fetch data, check status, or remotely control systems. And Bash is the perfect tool for calling APIs—curl is available on every server, jq parses JSON lightning-fast, and you can integrate it into any script without installing additional libraries or frameworks.

This article covers calling APIs with curl, parsing JSON responses with jq, handling authentication, implementing retry logic, and includes a practical script for automatically creating GitHub Issues when deploys fail.


Calling APIs with curl

Basic Methods

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# GET — retrieve data
curl -s "https://api.example.com/status"

# POST — create new data
curl -s -X POST \
    -H "Content-Type: application/json" \
    -d '{"title": "Bug report", "body": "Something went wrong"}' \
    "https://api.example.com/issues"

# PUT — update existing data
curl -s -X PUT \
    -H "Content-Type: application/json" \
    -d '{"status": "resolved"}' \
    "https://api.example.com/issues/42"

# DELETE — remove data
curl -s -X DELETE "https://api.example.com/issues/42"

Headers and Authentication

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Bearer token (most common)
curl -s -H "Authorization: Bearer $API_TOKEN" \
    "https://api.example.com/me"

# Basic auth
curl -s -u "username:password" \
    "https://api.example.com/private"

# Custom headers
curl -s -H "Accept: application/vnd.github.v3+json" \
    -H "X-Custom-Header: value" \
    "https://api.example.com/data"

Essential Flags

FlagMeaningWhen to Use
-sSilent — hide progress barAlways use in scripts
-SShow errors when combined with -sAlways pair with -s
-fFail silently on HTTP errorsAvoid parsing error pages
-w "%{http_code}"Output HTTP status codeCheck response status
-o /dev/nullSuppress output bodyOnly need status check
-LFollow redirectsAPI returns 301/302
--connect-timeoutConnection timeoutPrevent infinite hangs
--retry 3Auto-retry on failureHandle network blips
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Example: API health check with timeout and retry
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
    --connect-timeout 5 --retry 3 --retry-delay 2 \
    "https://api.example.com/health")

if [[ "$HTTP_CODE" -eq 200 ]]; then
    echo "API is healthy"
else
    echo "API returned status $HTTP_CODE"
fi

JSON Parsing with jq

jq is a lightweight command-line JSON processor—extremely powerful when combined with curl.

Installation

1
2
3
4
5
6
7
8
# Debian/Ubuntu
apt-get install -y jq

# CentOS/RHEL
yum install -y jq

# macOS
brew install jq

Basic Operations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Extract simple value
echo '{"name": "myapp", "version": "2.1.0"}' | jq -r '.name'
# Output: myapp

# Extract nested value
echo '{"repo": {"owner": "org", "name": "api"}}' | jq -r '.repo.name'
# Output: api

# Get array elements
echo '{"tags": ["dev", "prod", "staging"]}' | jq -r '.tags[]'
# Output:
# dev
# prod
# staging

# Count array elements
echo '{"users": [{"id":1},{"id":2},{"id":3}]}' | jq '.users | length'
# Output: 3

# Filter array
echo '[{"name":"a","status":"ok"},{"name":"b","status":"fail"}]' | jq '[.[] | select(.status == "fail")]'
# Output: [{"name":"b","status":"fail"}]

# Transform data
echo '{"first":"John","last":"Doe"}' | jq '{fullName: "\(.first) \(.last)"}'
# Output: {"fullName": "John Doe"}

Combining curl + jq

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Get repo name and star count from GitHub API
REPO_INFO=$(curl -s "https://api.github.com/repos/cli/cli")
NAME=$(echo "$REPO_INFO" | jq -r '.full_name')
STARS=$(echo "$REPO_INFO" | jq -r '.stargazers_count')
echo "$NAME has $STARS stars"

# List open issues
curl -s "https://api.github.com/repos/owner/repo/issues?state=open" | \
    jq -r '.[] | "[\(.number)] \(.title)"'

# Get latest commit message
curl -s "https://api.github.com/repos/owner/repo/commits?per_page=1" | \
    jq -r '.[0] | "\(.commit.author.name): \(.commit.message)"'

Retry Logic in Bash

When calling APIs from scripts, networks can have intermittent failures. Retry logic enables automatic retries:

Basic Retry Function

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
retry() {
    local max_attempts="$1"
    local delay="$2"
    shift 2
    local cmd=("$@")

    for ((attempt=1; attempt<=max_attempts; attempt++)); do
        if "${cmd[@]}"; then
            return 0
        fi
        echo "[WARN] Attempt $attempt/$max_attempts failed, retrying in ${delay}s..." >&2
        sleep "$delay"
    done

    echo "[ERROR] All $max_attempts attempts failed" >&2
    return 1
}

# Usage
retry 3 2 curl -s "https://api.example.com/flaky-endpoint"

Retry with Exponential Backoff

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
retry_with_backoff() {
    local max_attempts="${1:-3}"
    local base_delay="${2:-1}"
    shift 2
    local cmd=("$@")

    for ((attempt=1; attempt<=max_attempts; attempt++)); do
        if "${cmd[@]}"; then
            return 0
        fi

        local delay=$((base_delay * (2 ** (attempt - 1))))
        echo "[WARN] Attempt $attempt/$max_attempts failed, retrying in ${delay}s..." >&2
        sleep "$delay"
    done

    return 1
}

# Usage: retry 3 times, delay 1s → 2s → 4s
retry_with_backoff 3 1 curl -s "https://api.example.com/data"

Practical Example: Auto-Create GitHub Issue on Deploy Failure

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#!/usr/bin/env bash
# ==============================================================================
# Script : github-issue-on-failure.sh
# Purpose: Automatically create GitHub Issue when pipeline deploy fails
# ==============================================================================

set -euo pipefail

# Configuration
readonly GITHUB_TOKEN="${GITHUB_TOKEN:?GITHUB_TOKEN is required}"
readonly GITHUB_REPO="${GITHUB_REPO:?GITHUB_REPO is required (owner/repo)}"
readonly API_BASE="https://api.github.com/repos/${GITHUB_REPO}"
readonly SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"

# Pipeline info (from CI/CD environment)
readonly PIPELINE_URL="${CI_PIPELINE_URL:-${BUILD_URL:-unknown}}"
readonly COMMIT_SHA="${CI_COMMIT_SHA:-$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')}"
readonly COMMIT_MSG="${CI_COMMIT_MESSAGE:-$(git log -1 --pretty=%B 2>/dev/null || echo 'unknown')}"
readonly DEPLOY_ENV="${DEPLOY_ENV:-production}"
readonly TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

# Headers
AUTH_HEADER="Authorization: Bearer ${GITHUB_TOKEN}"
ACCEPT_HEADER="Accept: application/vnd.github.v3+json"

# GitHub API wrapper
github_api() {
    local method="$1"
    local endpoint="$2"
    local data="${3:-}"

    local args=(
        -s -f -S
        -X "$method"
        -H "$AUTH_HEADER"
        -H "$ACCEPT_HEADER"
        -H "Content-Type: application/json"
    )

    if [[ -n "$data" ]]; then
        args+=(-d "$data")
    fi

    curl "${args[@]}" "${API_BASE}${endpoint}"
}

# Check for duplicate issues
check_duplicate() {
    local title="$1"
    local existing
    existing=$(github_api GET "/issues?state=open&labels=auto-deploy-failure&per_page=100" 2>/dev/null || echo "[]")

    echo "$existing" | jq -r '.[].title' | grep -qF "$title"
}

# Create GitHub Issue
create_issue() {
    local title="$1"
    local body="$2"

    if check_duplicate "$title"; then
        echo "[INFO] Duplicate issue found, skipping creation"
        return 0
    fi

    local payload
    payload=$(jq -n \
        --arg title "$title" \
        --arg body "$body" \
        '{
            title: $title,
            body: $body,
            labels: ["auto-deploy-failure", "ops"],
            assignees: []
        }')

    local response
    response=$(github_api POST "/issues" "$payload")

    if [[ $? -eq 0 ]]; then
        local issue_url
        issue_url=$(echo "$response" | jq -r '.html_url')
        local issue_number
        issue_number=$(echo "$response" | jq -r '.number')
        echo "[INFO] Created issue #${issue_number}: ${issue_url}"
    else
        echo "[ERROR] Failed to create issue" >&2
        return 1
    fi
}

# Send Slack notification
notify_slack() {
    local message="$1"
    if [[ -n "$SLACK_WEBHOOK" ]]; then
        curl -s -X POST -H 'Content-type: application/json' \
            --data "{\"text\":\"${message}\"}" "$SLACK_WEBHOOK" >/dev/null 2>&1
    fi
}

# Main
main() {
    local fail_reason="${1:-Unknown failure}"

    local issue_title="[DEPLOY FAIL] ${DEPLOY_ENV}${COMMIT_SHA}$(date '+%Y-%m-%d')"
    local issue_body="## Deploy Failure Report

**Environment:** \`${DEPLOY_ENV}\`
**Commit:** \`${COMMIT_SHA}\`
**Time:** ${TIMESTAMP}
**Pipeline:** ${PIPELINE_URL}

### Commit Message
\`\`\`
${COMMIT_MSG}
\`\`\`

### Failure Reason
${fail_reason}

### Required Actions
- [ ] Check pipeline logs
- [ ] Verify deployment artifacts
- [ ] Confirm service health
- [ ] Update issue when resolved

---
*Auto-generated by deployment monitoring script*"

    echo "Creating GitHub issue for failed deploy..."
    create_issue "$issue_title" "$issue_body"

    local slack_msg="*[DEPLOY FAILURE]* ${DEPLOY_ENV}\nCommit: \`${COMMIT_SHA}\`\nReason: ${fail_reason}\nPipeline: ${PIPELINE_URL}"
    notify_slack -e "$slack_msg"
}

main "$@"

Pipeline Integration

1
2
3
4
5
6
7
8
9
# GitHub Actions example
deploy:
  script:
    - bash deploy.sh
  after_script:
    - |
      if [[ "$CI_JOB_STATUS" == "failed" ]]; then
        bash .github/scripts/github-issue-on-failure.sh "Deploy job failed"
      fi

Sample Output

1
2
Creating GitHub issue for failed deploy...
[INFO] Created issue #247: https://github.com/org/repo/issues/247

The GitHub Issue is created with:

  • Title: [DEPLOY FAIL] production — a1b2c3d — 2026-09-03
  • Labels: auto-deploy-failure, ops
  • Body containing environment, commit info, pipeline URL, and checklist

Deployment Notes & Best Practices

  • Token security: Always store GITHUB_TOKEN as a secret variable—never hardcode it in scripts. GitHub tokens need the repo scope to create issues.
  • Duplicate prevention: The script checks for duplicate issues before creating—preventing spam when pipelines fail repeatedly in succession.
  • jq dependency: Ensure jq is installed on the runner/CI machine. Most CI images include it, but if not, add apt-get install -y jq to the pipeline.
  • Rate limiting: GitHub API has rate limits (5000 requests/hour for authenticated users). This script makes few requests so it’s not an issue, but when writing scripts that call APIs heavily, check the X-RateLimit-Remaining header.
  • Multi-platform: The script uses GitHub API, but you can replace endpoints to create issues on GitLab, Jira, or any issue tracking system with a REST API.

Conclusion

Bash and REST API is an extremely powerful combination in DevOps. From simple API calls with curl, complex JSON parsing with jq, retry logic implementation, to automatically creating GitHub Issues on deploy failures—all can be accomplished with just Bash without heavy frameworks.

In the next post, we will explore Bash and Cloud CLI: managing AWS, GCP, Azure from Bash, querying JSON output, and automating cloud tasks.

RESPONSES & DISCUSSION