Operator docsJungle GridSectionAPI
Get startedCLI ReferenceAPI ReferencePortalTemplatesMCP IntegrationRelease notesBeginner resources

REST API

Build Against The Jungle Grid Execution API

Use the public orchestrator API from your own backend, workflow builder, or agent platform to estimate, submit, monitor, cancel, and retrieve outputs for Jungle Grid jobs.

Integration contract

  • Default base URL: https://api.junglegrid.dev. Set JUNGLE_GRID_API only for staging, local development, or private deployments.
  • Send API keys as Authorization: Bearer jg_... from server-side code only. Never expose keys in browser code.
  • Estimate before submit when users need a cost or capacity preview. Submit, cancel, and artifact download URL creation are side-effecting operations.
  • The public job lifecycle includes estimate, submit, list, status, runtime, logs, live logs, cancellation, artifact listing, and temporary artifact download URLs.

01

Authentication and scopes

Public API routes accept a browser-issued JWT session or a scoped API key. External integrations should use scoped API keys generated in the portal and keep them on the server. API keys are opaque jg_... values; the exact suffix is not part of the contract.

  • A missing or revoked key returns 401 UNAUTHORIZED.
  • A valid key without the required scope returns 403 FORBIDDEN.
  • Workspace-bound API keys can access only jobs and credentials in their bound workspace.
  • All examples below use placeholders; do not paste real keys into documentation, browser bundles, or public repos.
Headers
Authorization: Bearer jg_...
Content-Type: application/json
Accept: application/json
Common scopes
jobs:estimate  estimate jobs before spending credits
jobs:submit    submit jobs and cancel running work
jobs:write     broader write scope accepted by job submit/cancel routes
jobs:read      list jobs, read status/runtime/logs/artifacts
logs:read      read job logs
registry:read  list saved registry and Hugging Face credentials
registry:write create/delete saved registry and Hugging Face credentials

02

Execution lifecycle endpoints

These routes are the stable public contract for workflow builders. Jobs move through public statuses such as pending, queued, assigned, running, completed, failed, rejected, and cancelled. Terminal statuses are completed, failed, rejected, and cancelled.

  • POST /v1/jobs/estimate is read-only and does not start compute.
  • POST /v1/jobs queues real work and may reserve or consume credits based on the verified estimate and billing state.
  • POST /v1/jobs/{job_id}/cancel is side-effecting; it marks a non-terminal job cancelled and triggers managed teardown where applicable.
  • Common errors include INVALID_REQUEST, JOB_SCREENING_BLOCKED, INSUFFICIENT_FUNDS, MAINTENANCE_ACTIVE, CPU_ONLY_WORKLOAD_UNSUPPORTED, ACCELERATOR_REQUIREMENT_UNSPECIFIED, NOT_FOUND, FORBIDDEN, and CONFLICT.
Estimate job - read-only
POST /v1/jobs/estimate
Scope: jobs:estimate, jobs:read, or jobs:write
Body:
{
  "name": "estimate-demo",
  "image": "python:3.11",
  "workload_type": "batch",
  "model_size_gb": 1,
  "command": ["python", "-c", "print(42)"],
  "optimize_for": "balanced"
}
Response fields include:
available, classification, routing, capacity,
estimated_hourly_rate_usd, estimated_cost_min_usd,
estimated_cost_max_usd, estimated_queue_wait_min_sec,
estimated_start_time_min, warnings, screening
Submit job - starts real work
POST /v1/jobs
Scope: jobs:submit or jobs:write
Body:
{
  "name": "curl-submit",
  "image": "python:3.11",
  "workload_type": "batch",
  "model_size_gb": 1,
  "command": ["python", "-c", "print(42)"]
}
202 response:
{
  "job_id": "job_...",
  "status": "queued",
  "queued_at": "2026-06-02T12:00:00Z"
}
List and inspect
GET /v1/jobs?limit=20&cursor=0&status=running
Scope: jobs:read

GET /v1/jobs/{job_id}
Scope: jobs:read or jobs:write

GET /v1/jobs/{job_id}/runtime
Scope: jobs:read or jobs:write
Cancel job - stops real work
POST /v1/jobs/{job_id}/cancel
Scope: jobs:submit or jobs:write
Body: { "reason": "user requested stop" }
200 response:
{
  "job_id": "job_...",
  "status": "cancelled",
  "status_reason": "user requested stop"
}
Lifecycle cURL
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":"batch","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":"batch","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"

03

Logs and artifacts

Logs and managed artifacts are public REST contracts. Availability still depends on the runtime path and storage configuration: managed providers can report unsupported or delayed runtime fields when the upstream provider does not expose stdout, stderr, or exit code.

  • Writing regular files under /workspace/artifacts in a managed job makes them eligible for automatic artifact upload.
  • Artifact download URL creation is side-effecting because it mints a temporary signed URL.
  • Signed URLs are temporary secrets; do not log them or share them publicly.
  • If artifact storage is not configured or under maintenance, artifact routes return ARTIFACTS_UNAVAILABLE or MAINTENANCE_ACTIVE.
Fetch recent logs
GET /v1/jobs/{job_id}/logs?tail=100
Scope: logs:read, jobs:read, or jobs:write
Query:
  tail=1..500
  limit=1..500
  cursor=<next_cursor>
  stream=stdout|stderr|all
Response:
{
  "job_id": "job_...",
  "status": "running",
  "items": [
    {
      "entry_id": 42,
      "stream": "stdout",
      "source": "managed-wrapper",
      "message": "hello",
      "created_at": "2026-06-02T12:00:00Z"
    }
  ],
  "next_cursor": 42,
  "has_more": false
}
Stream live logs
GET /v1/jobs/{job_id}/logs/live?cursor=0
Scope: jobs:read or jobs:write
Response: text/event-stream
Events include status, log, notice, heartbeat, and terminal updates.
Artifacts
GET /v1/jobs/{job_id}/artifacts
Scope: jobs:read or jobs:write

POST /v1/jobs/{job_id}/artifacts/{artifact_id}/download
Scope: jobs:read or jobs:write
200 response:
{
  "artifact": {
    "artifact_id": "art_...",
    "job_id": "job_...",
    "filename": "output.json",
    "content_type": "application/json",
    "size_bytes": 123,
    "status": "ready",
    "ready": true
  },
  "url": "https://signed-download.example/...",
  "expires_at": "2026-06-02T12:15:00Z"
}
cURL artifact retrieval
ARTIFACT_ID=$(curl -sS \
  -H "Authorization: Bearer $JUNGLE_GRID_API_KEY" \
  "$JUNGLE_GRID_API/v1/jobs/$JOB_ID/artifacts" | jq -r '.artifacts[0].artifact_id')

curl -sS -X POST \
  -H "Authorization: Bearer $JUNGLE_GRID_API_KEY" \
  "$JUNGLE_GRID_API/v1/jobs/$JOB_ID/artifacts/$ARTIFACT_ID/download"

04

Callbacks and webhook security

Job submission accepts per-job callback settings. Jungle Grid sends terminal lifecycle callbacks to the configured HTTPS URL and includes both configured bearer auth and Jungle Grid callback metadata headers.

  • callback_url must use HTTPS unless it targets localhost.
  • callback_auth_token is sent back as Authorization: Bearer <token>; treat it as a secret.
  • Callback requests include X-JungleGrid-Callback-Version, X-JungleGrid-Callback-Timestamp, X-JungleGrid-Job-ID, and X-JungleGrid-Job-Status.
  • Workspace webhook signing-secret and delivery inspection routes exist for the portal workflow; use the portal webhooks page for account-level setup.
Submit with callback fields
{
  "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"
  }
}

05

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", "-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.

06

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 workload classification, route status, live versus managed capacity counts, 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.
  • GET /v1/jobs/{job_id}/logs and GET /v1/jobs/{job_id}/logs/live expose stored and streamed runtime logs where the runtime path reports them.
  • GET /v1/jobs/{job_id}/artifacts and POST /v1/jobs/{job_id}/artifacts/{artifact_id}/download expose managed artifacts and temporary signed download URLs where artifact storage is configured.
  • 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.
Primary job routes
POST /v1/jobs/estimate
POST /v1/jobs
GET /v1/jobs
GET /v1/jobs/{job_id}
GET /v1/jobs/{job_id}/runtime
GET /v1/jobs/{job_id}/logs
GET /v1/jobs/{job_id}/logs/live
POST /v1/jobs/{job_id}/cancel
GET /v1/jobs/{job_id}/artifacts
POST /v1/jobs/{job_id}/artifacts/{artifact_id}/download
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"
  }
}
Estimate availability fields
{
  "classification": { "requires_gpu": true, "acceleration_requirement": "gpu_required" },
  "routing": { "route_status": "provisionable", "selected_route_source": "managed_capacity_profile" },
  "capacity": { "live_candidate_count": 0, "managed_profile_count": 40, "estimate_source": "managed_capacity_profile" }
}

07

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

08

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

09

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.

The job lifecycle routes documented above are intended to be sufficient for workflow integrations such as a Langflow-style bundle without inventing endpoint behavior. Live paid execution still requires authenticated manual verification.