Batch API Operations

POST /api/v1/batch

Batch API Operations

Overview

The Batch Operations endpoint allows clients to bundle multiple API calls into a single HTTP request. Instead of making 50 individual requests to rate 50 domains, a client sends one batch request containing all 50 operations. The platform dispatches them in parallel using the BEAM's lightweight process model, collects the results, and returns them in a single response.

Batch operations address two practical problems. First, they reduce HTTP overhead: connection setup, TLS handshake, header serialization, and round-trip latency are paid once instead of per operation. Second, they simplify rate limit management: a batch of 50 operations counts as one request against the batch endpoint's rate limit, not 50 requests against 50 individual endpoint limits.

The batch system leverages Elixir's Task.async_stream/3 to dispatch operations concurrently across available BEAM schedulers. Each operation in the batch is executed independently, so a failure in one operation does not affect the others. Results are returned in the same order as the input operations, with each result including its own success/error status.

Batch operations are particularly valuable for EASM workflows that need to rate or scan multiple domains, agent fleet management operations that target multiple agents, and monitoring integrations that need to check multiple endpoints simultaneously.

Endpoint

POST /api/v1/batch

Executes multiple API operations in a single request with parallel dispatch.

Port: 4004 Content-Type: application/json

Authentication

Requires a valid API token. The token must have appropriate scopes for each operation in the batch. If the token lacks scope for any operation, that individual operation returns a 403 error in the results while other operations proceed normally.

Authorization: Bearer <api_token>

Request

Headers

HeaderRequiredDescription
AuthorizationYesBearer token with scopes for all included operations
Content-TypeYesMust be application/json
X-Request-IDNoClient-provided correlation ID

Body Parameters

ParameterTypeRequiredDefaultDescription
operationsarrayYes--Array of operation descriptors (max 100)
options.parallelbooleanNotrueExecute operations in parallel (false for sequential)
options.stop_on_errorbooleanNofalseStop remaining operations after first error (sequential only)
options.timeout_msintegerNo60000Overall batch timeout in milliseconds

Operation Descriptor

Each operation in the batch describes a single API call:

FieldTypeRequiredDescription
idstringYesClient-assigned operation ID (returned in results for correlation)
methodstringYesHTTP method: GET or POST
pathstringYesAPI path (e.g., /api/v1/perimeter/rating)
paramsobjectNoQuery parameters (for GET) or body parameters (for POST)

Example Request Body

{
  "operations": [
    {
      "id": "rate-domain-1",
      "method": "GET",
      "path": "/api/v1/perimeter/rating",
      "params": { "domain": "example.com" }
    },
    {
      "id": "rate-domain-2",
      "method": "GET",
      "path": "/api/v1/perimeter/rating",
      "params": { "domain": "example.org" }
    },
    {
      "id": "rate-domain-3",
      "method": "GET",
      "path": "/api/v1/perimeter/rating",
      "params": { "domain": "example.net" }
    },
    {
      "id": "scan-domain-1",
      "method": "POST",
      "path": "/api/v1/perimeter/discover",
      "params": {
        "domain": "example.com",
        "options": { "passive_only": true }
      }
    },
    {
      "id": "agent-status",
      "method": "GET",
      "path": "/api/v1/agents/status",
      "params": { "domain": "security" }
    }
  ],
  "options": {
    "parallel": true,
    "timeout_ms": 60000
  }
}

Response

Success Response (200 OK)

The response contains results for every operation in the batch, in the same order as the input. Each result includes the original operation ID, the HTTP status code, and the response body.

{
  "ok": true,
  "data": {
    "batch_id": "batch_2026021210300001",
    "total_operations": 5,
    "succeeded": 4,
    "failed": 1,
    "execution_time_ms": 2847,
    "results": [
      {
        "id": "rate-domain-1",
        "status": 200,
        "ok": true,
        "data": {
          "domain": "example.com",
          "rating": { "grade": "B", "score": 780 }
        },
        "execution_time_ms": 234
      },
      {
        "id": "rate-domain-2",
        "status": 200,
        "ok": true,
        "data": {
          "domain": "example.org",
          "rating": { "grade": "A", "score": 870 }
        },
        "execution_time_ms": 189
      },
      {
        "id": "rate-domain-3",
        "status": 404,
        "ok": false,
        "error": {
          "code": "no_scan_data",
          "message": "No discovery data available for example.net"
        },
        "execution_time_ms": 12
      },
      {
        "id": "scan-domain-1",
        "status": 200,
        "ok": true,
        "data": {
          "domain": "example.com",
          "scan_id": "scan_2026021210300042",
          "summary": { "total_assets": 23, "risk_score": 72.4 }
        },
        "execution_time_ms": 2341
      },
      {
        "id": "agent-status",
        "status": 200,
        "ok": true,
        "data": {
          "fleet_summary": { "total_agents": 20, "healthy": 18, "degraded": 2 }
        },
        "execution_time_ms": 23
      }
    ]
  },
  "meta": {
    "request_id": "req_batch_001",
    "execution_time_ms": 2847
  }
}

Response Fields

FieldTypeDescription
data.batch_idstringUnique batch execution identifier
data.total_operationsintegerNumber of operations in the batch
data.succeededintegerNumber of operations that returned 2xx
data.failedintegerNumber of operations that returned 4xx or 5xx
data.execution_time_msnumberTotal batch execution wall-clock time
data.resultsarrayPer-operation results (same order as input)
data.results[].idstringClient-assigned operation ID (from input)
data.results[].statusintegerHTTP status code for this operation
data.results[].okbooleanWhether the operation succeeded
data.results[].dataobjectOperation response data (on success)
data.results[].errorobjectOperation error details (on failure)
data.results[].execution_time_msnumberPer-operation execution time

Batch Limits

LimitValueDescription
Maximum operations per batch100Hard limit per request
Maximum request body size1 MBTotal batch request payload
Maximum batch timeout120 secondsOverall batch execution timeout
Per-operation timeout30 secondsIndividual operation timeout

Transaction Semantics

Batch operations do not provide transactional guarantees. Each operation executes independently:

  • No atomicity: Operations do not roll back if others fail
  • No ordering guarantees in parallel mode: Operations may complete in any order (results are reordered to match input order)
  • Independent error handling: Each operation succeeds or fails independently
  • Independent rate limiting: Individual operations within a batch are checked against per-endpoint rate limits

For workflows requiring transactional behavior, use sequential mode ("parallel": false) with "stop_on_error": true and implement compensating transactions in the client.

Code Examples

curl

# Batch rate multiple domains
curl -s -X POST \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "operations": [
      {"id": "d1", "method": "GET", "path": "/api/v1/perimeter/rating", "params": {"domain": "example.com"}},
      {"id": "d2", "method": "GET", "path": "/api/v1/perimeter/rating", "params": {"domain": "example.org"}},
      {"id": "d3", "method": "GET", "path": "/api/v1/perimeter/rating", "params": {"domain": "example.net"}}
    ]
  }' \
  http://localhost:4004/api/v1/batch | jq '.data.results[] | {id, grade: .data.rating.grade}'

# Batch with mixed endpoint types
curl -s -X POST \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "operations": [
      {"id": "health", "method": "GET", "path": "/api/v1/health"},
      {"id": "agents", "method": "GET", "path": "/api/v1/agents/list", "params": {"domain": "security"}},
      {"id": "rating", "method": "GET", "path": "/api/v1/perimeter/rating", "params": {"domain": "example.com"}}
    ]
  }' \
  http://localhost:4004/api/v1/batch | jq '.data | {succeeded, failed, execution_time_ms}'

Elixir

# Build a batch of rating checks
domains = ["example.com", "example.org", "example.net", "example.edu"]

operations = Enum.map(domains, fn domain ->
  %{
    id: "rate-#{domain}",
    method: "GET",
    path: "/api/v1/perimeter/rating",
    params: %{domain: domain}
  }
end)

{:ok, response} = HTTPoison.post(
  "http://localhost:4004/api/v1/batch",
  Jason.encode!(%{operations: operations}),
  [{"Authorization", "Bearer #{token}"}, {"Content-Type", "application/json"}]
)

%{"data" => %{"results" => results}} = Jason.decode!(response.body)

Enum.each(results, fn result ->
  case result do
    %{"ok" => true, "data" => %{"rating" => %{"grade" => grade}}} ->
      IO.puts("#{result["id"]}: #{grade}")
    %{"ok" => false, "error" => %{"message" => msg}} ->
      IO.puts("#{result["id"]}: ERROR - #{msg}")
  end
end)

Python

import requests

headers = {
    "Authorization": f"Bearer {api_token}",
    "Content-Type": "application/json"
}

# Batch rate 50 domains
domains = [f"domain{i}.com" for i in range(50)]

operations = [
    {
        "id": f"rate-{domain}",
        "method": "GET",
        "path": "/api/v1/perimeter/rating",
        "params": {"domain": domain}
    }
    for domain in domains
]

response = requests.post(
    "http://localhost:4004/api/v1/batch",
    headers=headers,
    json={"operations": operations, "options": {"parallel": True}}
)

batch = response.json()["data"]
print(f"Batch completed: {batch['succeeded']}/{batch['total_operations']} succeeded "
      f"in {batch['execution_time_ms']}ms")

# Process results
for result in batch["results"]:
    if result["ok"]:
        rating = result["data"]["rating"]
        print(f"  {result['id']}: {rating['grade']} ({rating['score']})")
    else:
        print(f"  {result['id']}: ERROR - {result['error']['message']}")

Error Responses

Batch-Level Errors

Status CodeError CodeDescription
400invalid_batchBatch request format is invalid
400too_many_operationsExceeded 100 operations per batch
401unauthorizedMissing or invalid authentication token
408batch_timeoutOverall batch execution exceeded timeout
413request_too_largeRequest body exceeds 1 MB limit
429rate_limitedBatch endpoint rate limit exceeded

Operation-Level Errors

Individual operations within a batch can return any error documented in Error Handling. These errors are contained within the operation's result object and do not affect the batch response status code (which is always 200 if the batch itself was valid).

Rate Limits

Limit TypeValueWindow
Per token30 requests1 minute
Burst5 requests1 second
Max operations100Per batch request

Note: Individual operations within a batch are still checked against their per-endpoint rate limits. If an operation within a batch exceeds its endpoint limit, that operation returns 429 while other operations proceed.

Performance Characteristics

Batch operations benefit from the BEAM VM's lightweight process model. Each operation in a parallel batch spawns a dedicated Elixir process (approximately 2KB of memory per process). For a batch of 100 operations, this means approximately 200KB of additional memory overhead, with all operations executing concurrently across available CPU cores.

The total batch execution time is approximately equal to the slowest individual operation plus a small overhead for result aggregation (typically under 5ms). A batch of 50 rating lookups that each take 200ms completes in approximately 205ms rather than 10,000ms.

The Trinity Gate validates that batch results maintain consistency: every operation in a batch produces the same result it would produce if called individually. No cross-operation interference is permitted, and the No Mercy doctrine ensures that partial batch failures are reported transparently rather than masked.


Connect & Contribute

Created by Tomáš Korcak (korczis) | Open Source under GHL