Jungle Grid
API
Get startedCLI ReferenceAPI ReferencePortalMCP IntegrationRelease notes

REST API

Orchestrator Routes That Power The CLI And Portal

Jungle Grid exposes a small set of orchestrator-backed REST surfaces for auth, jobs, nodes, account linking, billing, and provider operations.

API shape

  • Authentication for app flows uses browser/session routes under /auth and Bearer-protected resource routes under /v1.
  • Job submission and estimation are server-authoritative and return abstract workload-facing data rather than placement internals.
  • Per-job routing overrides are additive and optional; they constrain placement without requiring exact GPU selection.
  • Billing now exposes USD-only topups, balance/history routes, and the Paystack webhook route.

01

Auth and device flow

CLI authentication uses the device flow. The CLI registers a device code, the browser completes login as the currently signed-in account, and the CLI polls for the resulting token. Registration triggers an email OTP for address verification before the account is active.

  • Device completion is tied to the authenticated browser session, not arbitrary email entry.
  • Registration sends a 6-digit OTP to the registered email address (10-minute TTL).
  • POST /auth/otp/verify marks the account email-verified and triggers the welcome email.
  • POST /auth/otp/resend generates a fresh code if the original expired.
Device flow routes
POST /auth/device
GET /auth/token?device_code=...
POST /auth/complete
Browser account flows
POST /auth/register
POST /auth/signin
GET /auth/google
POST /auth/google/link/start
Email OTP verification
POST /auth/otp/verify   # { code: "123456" }
POST /auth/otp/resend   # regenerate and resend code

02

API keys are not CLI sessions

Bearer auth exists in two client-side forms. Browser and CLI flows use account JWT sessions. Direct REST and MCP automation can use scoped API keys with the jg_ prefix. Do not expect the jungle CLI to read JUNGLE_GRID_API_KEY; it does not.

  • Use Authorization: Bearer jg_... only when you are calling REST directly or running the MCP server.
  • Use ~/.jungle-grid/credentials.json when you are running jungle submit from the CLI.
  • A jobs:write key can submit and cancel jobs but cannot list jobs unless it also has jobs:read.
  • An unknown, revoked, expired, or IP-disallowed API key returns 401 or 403 before the route handler runs.
Direct REST submit with API key
export JUNGLE_GRID_API="https://api.junglegrid.dev"
export JUNGLE_GRID_API_KEY="jg_..."

curl -X POST "$JUNGLE_GRID_API/v1/jobs" \
  -H "Authorization: Bearer $JUNGLE_GRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "api-smoke-test",
    "workload_type": "inference",
    "model_size_gb": 7,
    "image": "ghcr.io/acme/infer:latest",
    "command": "python",
    "args": ["serve.py"],
    "optimize_for": "balanced"
  }'
Job API key scopes
POST /v1/jobs                    jobs:write
POST /v1/jobs/estimate           jobs:read or jobs:write
GET  /v1/jobs                    jobs:read
GET  /v1/jobs/{job_id}           jobs:read or jobs:write
GET  /v1/jobs/{job_id}/runtime   jobs:read or jobs:write
GET  /v1/jobs/{job_id}/logs/live jobs:read or jobs:write
POST /v1/jobs/{job_id}/cancel    jobs:write
Registry API key scopes
GET    /v1/registry-credentials                  registry:read
POST   /v1/registry-credentials                  registry:write
DELETE /v1/registry-credentials/{credential_id}  registry:write
For a Render worker that wants the CLI UX, inject the CLI credentials JSON. For a Render worker that wants API-key automation, skip the CLI and call /v1/jobs directly.

03

Integration guides

Use these examples when Jungle Grid is called from your own backend service. Keep JUNGLE_GRID_API_KEY on the server, forward only the fields your app allows, and return the orchestrator response to your client or worker.

  • Do not put JUNGLE_GRID_API_KEY in browser code, mobile apps, static sites, or public repositories.
  • Validate your own request body before forwarding it so users cannot submit arbitrary images or commands through your backend.
  • Use POST /v1/jobs/estimate before POST /v1/jobs when the user needs a cost or capacity preview.
  • Callback URLs must use HTTPS unless they target localhost; callback_auth_token is sent back as Authorization: Bearer <token>.
  • Callback requests include X-JungleGrid-Callback-Version, X-JungleGrid-Callback-Timestamp, X-JungleGrid-Job-ID, and X-JungleGrid-Job-Status headers.
FastAPI / Python
# pip install fastapi uvicorn httpx
import os
import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

API_URL = os.getenv("JUNGLE_GRID_API", "https://api.junglegrid.dev").rstrip("/")
API_KEY = os.environ["JUNGLE_GRID_API_KEY"]

app = FastAPI()

class SubmitRequest(BaseModel):
    name: str = "fastapi-submit"
    image: str
    workload_type: str = "inference"
    model_size_gb: float = 7
    command: str | None = None
    args: list[str] = []
    optimize_for: str = "balanced"

@app.post("/jobs")
async def submit_job(req: SubmitRequest):
    headers = {
        "Authorization": "Bearer " + API_KEY,
        "Content-Type": "application/json",
    }
    payload = req.model_dump(exclude_none=True)

    async with httpx.AsyncClient(timeout=30) as client:
        resp = await client.post(API_URL + "/v1/jobs", headers=headers, json=payload)

    data = resp.json()
    if resp.status_code >= 400:
        raise HTTPException(status_code=resp.status_code, detail=data)
    return data
Node.js / Express
// npm install express
import express from "express";

const API_URL = (process.env.JUNGLE_GRID_API ?? "https://api.junglegrid.dev").replace(/\/+$/, "");
const API_KEY = process.env.JUNGLE_GRID_API_KEY;

if (!API_KEY) {
  throw new Error("JUNGLE_GRID_API_KEY is required");
}

const app = express();
app.use(express.json());

app.post("/jobs", async (req, res, next) => {
  try {
    const payload = {
      name: req.body.name ?? "express-submit",
      workload_type: req.body.workload_type ?? "inference",
      model_size_gb: req.body.model_size_gb ?? 7,
      image: req.body.image,
      command: req.body.command,
      args: req.body.args ?? [],
      optimize_for: req.body.optimize_for ?? "balanced",
    };

    const upstream = await fetch(API_URL + "/v1/jobs", {
      method: "POST",
      headers: {
        Authorization: "Bearer " + API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    const data = await upstream.json();
    res.status(upstream.status).json(data);
  } catch (err) {
    next(err);
  }
});

app.listen(process.env.PORT ?? 3000);
Go HTTP handler
package main

import (
	"bytes"
	"encoding/json"
	"io"
	"net/http"
	"os"
	"strings"
	"time"
)

func submitJob(w http.ResponseWriter, r *http.Request) {
	apiURL := strings.TrimRight(os.Getenv("JUNGLE_GRID_API"), "/")
	if apiURL == "" {
		apiURL = "https://api.junglegrid.dev"
	}
	apiKey := os.Getenv("JUNGLE_GRID_API_KEY")
	if apiKey == "" {
		http.Error(w, "JUNGLE_GRID_API_KEY is required", http.StatusInternalServerError)
		return
	}

	var payload map[string]any
	if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
		http.Error(w, "invalid json", http.StatusBadRequest)
		return
	}
	if payload["name"] == nil {
		payload["name"] = "go-submit"
	}
	if payload["workload_type"] == nil {
		payload["workload_type"] = "inference"
	}

	body, _ := json.Marshal(payload)
	req, _ := http.NewRequestWithContext(r.Context(), http.MethodPost, apiURL+"/v1/jobs", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway)
		return
	}
	defer resp.Body.Close()

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(resp.StatusCode)
	_, _ = io.Copy(w, resp.Body)
}
cURL smoke test
export JUNGLE_GRID_API="https://api.junglegrid.dev"
export JUNGLE_GRID_API_KEY="jg_..."

curl -sS -X POST "$JUNGLE_GRID_API/v1/jobs/estimate" \
  -H "Authorization: Bearer $JUNGLE_GRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"curl-estimate","image":"python:3.11","workload_type":"inference","model_size_gb":1}'

JOB_ID=$(curl -sS -X POST "$JUNGLE_GRID_API/v1/jobs" \
  -H "Authorization: Bearer $JUNGLE_GRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"curl-submit","image":"python:3.11","workload_type":"inference","model_size_gb":1,"command":"python","args":["-c","print(42)"]}' \
  | jq -r .job_id)

curl -sS -H "Authorization: Bearer $JUNGLE_GRID_API_KEY" "$JUNGLE_GRID_API/v1/jobs/$JOB_ID"
curl -sS -H "Authorization: Bearer $JUNGLE_GRID_API_KEY" "$JUNGLE_GRID_API/v1/jobs/$JOB_ID/runtime"
Next.js route handler
// app/api/jungle/jobs/route.ts
const API_URL = (process.env.JUNGLE_GRID_API ?? "https://api.junglegrid.dev").replace(/\/+$/, "");

export async function POST(req: Request) {
  const apiKey = process.env.JUNGLE_GRID_API_KEY;
  if (!apiKey) {
    return Response.json({ error: "server is missing JUNGLE_GRID_API_KEY" }, { status: 500 });
  }

  const input = await req.json();
  const upstream = await fetch(API_URL + "/v1/jobs", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + apiKey,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: input.name ?? "nextjs-submit",
      image: input.image,
      workload_type: input.workload_type ?? "inference",
      model_size_gb: input.model_size_gb ?? 7,
      command: input.command,
      args: input.args ?? [],
      optimize_for: input.optimize_for ?? "balanced",
    }),
  });

  const data = await upstream.json();
  return Response.json(data, { status: upstream.status });
}
Submit with callback
curl -X POST "$JUNGLE_GRID_API/v1/jobs" \
  -H "Authorization: Bearer $JUNGLE_GRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "callback-demo",
    "image": "python:3.11",
    "workload_type": "batch",
    "command": "python",
    "args": ["-c", "print(42)"],
    "callback_url": "https://api.example.com/jungle/webhook",
    "callback_auth_token": "your-downstream-callback-token",
    "callback_metadata": {
      "request_id": "req_123",
      "customer_id": "cus_456"
    }
  }'
FastAPI webhook receiver
import os
from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
WEBHOOK_TOKEN = os.environ["JUNGLE_GRID_WEBHOOK_TOKEN"]

@app.post("/jungle/webhook")
async def jungle_webhook(request: Request):
    if request.headers.get("authorization") != "Bearer " + WEBHOOK_TOKEN:
        raise HTTPException(status_code=401, detail="invalid callback token")

    event = await request.json()
    job_id = event.get("job_id")
    status = event.get("status")
    correlation = event.get("correlation", {})

    # Update your database, notify users, or enqueue follow-up work here.
    return {"ok": True, "job_id": job_id, "status": status, "correlation": correlation}
Express webhook receiver
app.post("/jungle/webhook", express.json(), (req, res) => {
  const expected = "Bearer " + process.env.JUNGLE_GRID_WEBHOOK_TOKEN;
  if (req.get("authorization") !== expected) {
    return res.sendStatus(401);
  }

  const event = req.body;
  console.log("job callback", {
    job_id: event.job_id,
    status: event.status,
    correlation: event.correlation,
  });

  res.json({ ok: true });
});
Render / CI environment
JUNGLE_GRID_API=https://api.junglegrid.dev
JUNGLE_GRID_API_KEY=jg_...
JUNGLE_GRID_WEBHOOK_TOKEN=choose-a-random-downstream-token

# Required API key scopes:
# jobs:write for submit/cancel
# jobs:read for list/status/runtime/logs
# registry:read or registry:write only when managing private image credentials
These examples intentionally call REST directly. If you want to run the jungle CLI inside a worker instead, use the headless CLI credentials file documented in the CLI guide.

04

Jobs and runtime surfaces

Jobs are created and queried through Bearer-protected /v1 routes. The estimate endpoint uses the same draft job fields as submit and returns likely placement, runtime range, cost range, queue wait range, and estimated start window before confirmation. Optional constraints can be supplied per job while keeping the API intent-first.

  • POST /v1/jobs queues a new workload for an authenticated account.
  • GET /v1/jobs lists the caller's own jobs; GET /v1/jobs/{job_id} returns the detail view.
  • GET /v1/jobs/{job_id}/runtime exposes runtime tails and exit information when available.
  • Estimate responses now include estimated_hourly_rate_usd, min/max hourly range, estimated_queue_wait_min_sec, estimated_queue_wait_max_sec, estimated_start_time_min, estimated_start_time_max, and constraints_relaxed when soft preferences were auto-relaxed.
  • POST /v1/jobs/{job_id}/share awards +$3 (300 credits) once per completed job.
Primary job routes
POST /v1/jobs
GET /v1/jobs
GET /v1/jobs/{job_id}
GET /v1/jobs/{job_id}/runtime
POST /v1/jobs/estimate
Optional routing constraints
{
  "constraints": {
    "max_price_per_hour": 2.5,
    "preferred_gpu_family": "l4",
    "avoid_gpu_families": ["a100"],
    "region_preference": "us-east",
    "latency_priority": "high",
    "cost_priority": "balanced"
  }
}
Share to earn
POST /v1/jobs/{job_id}/share
# Response: { shared: true, credits_awarded: 300 }

05

Nodes, provider operations, and account linking

Public capacity discovery and provider-owned node operations are split. Providers also have an authenticated route for linking an additional business role to the same account.

  • GET /v1/nodes is public capacity discovery.
  • GET /v1/nodes/mine and POST /v1/nodes/register are provider-facing authenticated routes.
  • Registry credential routes let the portal and CLI save private image credentials explicitly.
Node and account routes
GET /v1/nodes
GET /v1/nodes/mine
POST /v1/nodes/register
GET /v1/nodes/{node_id}
POST /v1/account/roles
GET /v1/registry-credentials
POST /v1/registry-credentials

06

Billing, topups, and payouts

Billing surfaces are built around credits and use Paystack-backed wallet funding in USD. User and provider views share balance and history routes, while provider payout routes are currently unavailable as Jungle Grid standardizes on USD.

  • Users start USD wallet topups and verify them through the topup routes.
  • Provider payout endpoints remain present but currently return a temporary-unavailable response.
  • The Paystack webhook route is the settlement hook for billing events.
Billing routes
GET /v1/billing/balance
GET /v1/billing/history
POST /v1/billing/topups
POST /v1/billing/topups/verify
GET /v1/billing/payout-profile
POST /v1/billing/payout-profile
POST /v1/billing/payouts
POST /v1/payments/paystack/webhook

07

Implementation notes for consumers

The web portal and CLI both treat the orchestrator as the source of truth. Resource APIs require Bearer tokens, and the portal's public browser pages delegate session choice before handing off to authenticated /v1 routes.

This page is a product-facing route map, not a full OpenAPI spec. Use the orchestrator code and typed client wrappers when you need exact response contracts.