Mint from CI
Exchange an API key for a JWT, then mint a short URL from GitHub Actions or any CI shell — the one distribution recipe (not Zapier).
This is the one distribution asset for Phase 3: Postman for humans, this recipe for machines. Non-goal: Zapier, Slack bots, or browser extensions.
Prerequisites
- Mint an API key in the dashboard (Authentication) — format
snd_…. - Store it as a CI secret named
SHORTND_API_KEY(never commit it). - Optional: import the Postman collection for the same flow interactively.
Contract (two HTTP calls)
1) POST /api/v1/auth/token
Authorization: ApiKey $SHORTND_API_KEY
→ data.accessToken (JWT, ~15 minutes)
2) POST /api/v1/urls
Authorization: Bearer $SHORTND_ACCESS_TOKEN
Body: { "targetUrl": "https://…" }
→ data.shortUrl / data.id / data.shortCodeDo not send the long-lived key as Bearer.
Shell recipe (any CI)
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${SHORTND_BASE_URL:-https://shortnd.com}"
TARGET_URL="${1:?usage: $0 <targetUrl> [customSlug]}"
CUSTOM_SLUG="${2:-}"
: "${SHORTND_API_KEY:?set SHORTND_API_KEY}"
TOKEN_JSON=$(curl -sS -X POST "${BASE_URL}/api/v1/auth/token" \
-H "Authorization: ApiKey ${SHORTND_API_KEY}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{}')
ACCESS_TOKEN=$(python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["data"]["accessToken"])' <<<"$TOKEN_JSON")
if [[ -n "$CUSTOM_SLUG" ]]; then
BODY=$(python3 -c 'import json,sys; print(json.dumps({"targetUrl":sys.argv[1],"customSlug":sys.argv[2]}))' "$TARGET_URL" "$CUSTOM_SLUG")
else
BODY=$(python3 -c 'import json,sys; print(json.dumps({"targetUrl":sys.argv[1]}))' "$TARGET_URL")
fi
CREATE_JSON=$(curl -sS -X POST "${BASE_URL}/api/v1/urls" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "$BODY")
python3 -c 'import json,sys; d=json.load(sys.stdin); data=d["data"]; print(data["shortUrl"]); print("id="+str(data["id"])); print("shortCode="+data.get("shortCode",""))' <<<"$CREATE_JSON"Repo copy (same logic): scripts/mint-url-from-ci.sh.
export SHORTND_API_KEY=snd_… # from CI secrets
./scripts/mint-url-from-ci.sh 'https://example.com/launch' 'launch-2026'GitHub Actions
name: Mint Shortnd link
on:
workflow_dispatch:
inputs:
target_url:
description: Destination URL
required: true
custom_slug:
description: Optional custom slug
required: false
default: ''
jobs:
mint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Exchange key and mint URL
env:
SHORTND_API_KEY: ${{ secrets.SHORTND_API_KEY }}
SHORTND_BASE_URL: https://shortnd.com
run: |
chmod +x scripts/mint-url-from-ci.sh
./scripts/mint-url-from-ci.sh \
"${{ inputs.target_url }}" \
"${{ inputs.custom_slug }}"Use a dedicated key with only the scopes you need (urls:write). Rotate if it leaks.
Postman (same flow)
- Download public-api.postman_collection.json.
- Set collection variable
apiKey. - Run Auth → POST
/api/v1/auth/token—accessTokenis saved automatically. - Run URLs → POST
/api/v1/urls—id/shortUrl/shortCodeare saved automatically.
Details: Postman collection.
Ops note
Production Contabo runs an hourly token → create → delete smoke (scripts/smoke-public-api.sh). That proves the same auth path; it is not a substitute for your product CI mint step. See docs/ops/PUBLIC_API_SMOKE_CRON.md.