Skip to content

Vendor API Integration Guide

Chloe41427 edited this page Aug 31, 2026 · 5 revisions

Vendor API Integration Guide

This guide explains how vendors authenticate with the SDKMAN! State API using JWT tokens, and how to integrate this into your release pipeline.

Overview

The State API uses JWT (JSON Web Token) authentication. The flow is:

  1. Exchange your vendor credentials for a short-lived JWT token
  2. Use the token as a Bearer token on all subsequent API calls
  3. Request a new token when it expires (tokens are valid for 10 minutes)

Prerequisites

  • A vendor account provisioned by the SDKMAN! team (email + password)
  • The base URL of the State API (provided during onboarding)

Authentication with curl

Step 1: Obtain a JWT Token

curl -s -X POST https://state.sdkman.io/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "vendor@example.com",
    "password": "your-password"
  }'

Success (200):

{
  "token": "eyJhbGciOiJIUzI1NiIs..."
}

Possible errors:

Status Meaning
401 Invalid credentials
429 Rate limit exceeded (5 attempts/min) - wait and retry

Step 2: Use the Token

Include the token as a Bearer token in the Authorization header:

TOKEN="eyJhbGciOiJIUzI1NiIs..."

# Publish a version
curl -s -X POST https://state.sdkman.io/versions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "candidate": "gradle",
    "version": "9.7.1",
    "platform": "UNIVERSAL",
    "url": "https://services.gradle.org/distributions/gradle-9.7.1-all.zip"
  }'

Most candidates ship a single, platform-independent distribution — use "platform": "UNIVERSAL" and omit distribution (that field is Java-specific, for vendors like TEMURIN or CORRETTO). See Universal Releases vs Multi-Platform Releases below for when each applies.

Success: 204 No Content

Step 3: Handle Token Expiry

Tokens expire after 10 minutes. There are no refresh tokens - simply request a new one:

# Full example: login, capture token, publish
TOKEN=$(curl -sf -X POST https://state.sdkman.io/login \
  -H "Content-Type: application/json" \
  -d '{"email": "vendor@example.com", "password": "your-password"}' \
  | jq -r '.token')

curl -sf -X POST https://state.sdkman.io/versions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "candidate": "gradle",
    "version": "9.7.1",
    "platform": "UNIVERSAL",
    "url": "https://services.gradle.org/distributions/gradle-9.7.1-all.zip"
  }'

Integration with GitHub Actions

Store your credentials as repository secrets:

  • SDKMAN_VENDOR_EMAIL - your vendor email
  • SDKMAN_VENDOR_PASSWORD - your vendor password

Universal Releases

Most candidates (Gradle, Maven, Ant, SBT, and the like) ship a single, platform-independent archive. Publish once with platform: UNIVERSAL and no distribution:

name: Publish to SDKMAN!

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Authenticate with SDKMAN! State API
        id: auth
        run: |
          TOKEN=$(curl -sf -X POST https://state.sdkman.io/login \
            -H "Content-Type: application/json" \
            -d "{\"email\": \"${{ secrets.SDKMAN_VENDOR_EMAIL }}\", \"password\": \"${{ secrets.SDKMAN_VENDOR_PASSWORD }}\"}" \
            | jq -r '.token')
          echo "::add-mask::$TOKEN"
          echo "token=$TOKEN" >> "$GITHUB_OUTPUT"

      - name: Publish version
        run: |
          curl -sf -X POST https://state.sdkman.io/versions \
            -H "Authorization: Bearer ${{ steps.auth.outputs.token }}" \
            -H "Content-Type: application/json" \
            -d '{
              "candidate": "gradle",
              "version": "${{ github.event.release.tag_name }}",
              "platform": "UNIVERSAL",
              "url": "https://services.gradle.org/distributions/gradle-${{ github.event.release.tag_name }}-all.zip"
            }'

Note: ::add-mask:: ensures the token is redacted from workflow logs.

Multi-Platform Releases

For candidates distributed across multiple platforms (e.g. Java, where each vendor ships separate binaries per OS/architecture), authenticate once and publish each platform:

      - name: Authenticate
        id: auth
        run: |
          TOKEN=$(curl -sf -X POST https://state.sdkman.io/login \
            -H "Content-Type: application/json" \
            -d "{\"email\": \"${{ secrets.SDKMAN_VENDOR_EMAIL }}\", \"password\": \"${{ secrets.SDKMAN_VENDOR_PASSWORD }}\"}" \
            | jq -r '.token')
          echo "::add-mask::$TOKEN"
          echo "token=$TOKEN" >> "$GITHUB_OUTPUT"

      - name: Publish all platforms
        env:
          TOKEN: ${{ steps.auth.outputs.token }}
          VERSION: ${{ github.event.release.tag_name }}
        run: |
          BASE_URL="https://state.sdkman.io"

          for platform in LINUX_X64 LINUX_ARM64 MAC_X64 MAC_ARM64 WINDOWS_X64; do
            curl -sf -X POST "$BASE_URL/versions" \
              -H "Authorization: Bearer $TOKEN" \
              -H "Content-Type: application/json" \
              -d "{
                \"candidate\": \"java\",
                \"version\": \"$VERSION\",
                \"platform\": \"$platform\",
                \"distribution\": \"TEMURIN\",
                \"url\": \"https://github.com/adoptium/temurin21-binaries/releases/download/$VERSION/OpenJDK21U-jdk_${platform}.tar.gz\"
              }"
            echo "Published $platform"
          done

Other Operations

Delete a Version

curl -sf -X DELETE https://state.sdkman.io/versions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "candidate": "gradle",
    "version": "9.7.1",
    "platform": "UNIVERSAL"
  }'

Delete a Tag

curl -sf -X DELETE https://state.sdkman.io/versions/tags \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "candidate": "gradle",
    "tag": "latest",
    "platform": "UNIVERSAL"
  }'

Error Reference

Status Meaning
204 Success (no content)
400 Validation error (check request body)
401 Missing, invalid, or expired token
403 Token valid but not authorised for this candidate
404 Resource not found
429 Rate limit exceeded on login (5 attempts/min per IP)

Coming Soon: sdkman-release-action

The manual curl-based workflow described above is an interim integration path. We are retrofitting the sdkman-release-action to handle JWT authentication transparently under the hood.

Once available, your GitHub Actions workflow will simplify to something like:

      - name: Publish to SDKMAN!
        uses: sdkman/sdkman-release-action@main
        with:
          email: ${{ secrets.SDKMAN_VENDOR_EMAIL }}
          password: ${{ secrets.SDKMAN_VENDOR_PASSWORD }}
          candidate: gradle
          version: ${{ github.event.release.tag_name }}
          platform: UNIVERSAL
          url: https://example.com/releases/gradle-9.7.1-all.zip

No manual token management, no curl. The action will handle login, token lifecycle, and error handling for you.

Watch the sdkman-release-action repo for updates.


Need Help?

Join the SDKMAN! community on Discord - head to the #vendors channel.