NullSpend Docs

Actions API

Create, approve, reject, and track human-in-the-loop (HITL) actions. Actions let agents request human approval before executing sensitive operations.

Create, approve, reject, and track human-in-the-loop (HITL) actions. Actions let agents request human approval before executing sensitive operations.

See API Overview for authentication, pagination, errors, and ID formats.


List Actions

GET /api/actions

Retrieve actions for the current organization with optional status filtering.

Authentication

Session (dashboard)

Parameters

NameInTypeRequiredDescription
statusquerystringNoFilter by single status.
statusesquerystringNoComma-separated status list (e.g., "pending,approved").
limitqueryintegerNoPage size. 1–100, default 50.
cursorquerystringNoJSON-encoded cursor from a previous response.

Valid statuses: pending, approved, rejected, expired, executing, executed, failed

Request

# Requires dashboard session cookie
curl "https://nullspend.dev/api/actions?statuses=pending,approved&limit=20" \
  -H "Cookie: session=..."

Response

200 OK:

{
  "data": [
    {
      "id": "ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "agentId": "support-bot",
      "actionType": "send_email",
      "status": "pending",
      "payload": {
        "to": "customer@example.com",
        "subject": "Your refund has been processed"
      },
      "metadata": { "ticketId": "T-1234" },
      "createdAt": "2026-03-20T14:30:00.000Z",
      "approvedAt": null,
      "rejectedAt": null,
      "executedAt": null,
      "expiresAt": "2026-03-21T14:30:00.000Z",
      "expiredAt": null,
      "approvedBy": null,
      "rejectedBy": null,
      "result": null,
      "errorMessage": null,
      "environment": null,
      "sourceFramework": null
    }
  ],
  "cursor": null
}

Errors

CodeHTTPWhen
validation_error400Invalid status, limit, or cursor
authentication_required401No valid session

Create Action

POST /api/actions

Request human approval for a sensitive operation. The action starts in pending status and must be approved or rejected from the dashboard.

Authentication

API key

Parameters

NameInTypeRequiredDescription
agentIdbodystringYesAgent identifier. 1–255 chars.
actionTypebodystringYesOne of: send_email, http_post, http_delete, shell_command, db_write, file_write, file_delete.
payloadbodyobjectYesAction payload. Max 64 KB serialized, max 20 nesting levels.
metadatabodyobjectNoAdditional metadata. Max 16 KB serialized, max 20 nesting levels.
expiresInSecondsbodyintegerNoSeconds until the action expires. 0–2,592,000 (30 days). null for no expiry.
Idempotency-KeyheaderstringNoDeduplication key for idempotent retries.

Request

const res = await fetch("https://nullspend.dev/api/actions", {
  method: "POST",
  headers: {
    "X-NullSpend-Key": "ns_live_sk_abc123...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    agentId: "support-bot",
    actionType: "send_email",
    payload: {
      to: "customer@example.com",
      subject: "Your refund has been processed",
      body: "We've processed your $50 refund...",
    },
    metadata: { ticketId: "T-1234" },
    expiresInSeconds: 86400,
  }),
});
import requests

resp = requests.post(
    "https://nullspend.dev/api/actions",
    headers={"X-NullSpend-Key": "ns_live_sk_abc123..."},
    json={
        "agentId": "support-bot",
        "actionType": "send_email",
        "payload": {
            "to": "customer@example.com",
            "subject": "Your refund has been processed",
            "body": "We've processed your $50 refund...",
        },
        "metadata": {"ticketId": "T-1234"},
        "expiresInSeconds": 86400,
    },
)
curl -X POST https://nullspend.dev/api/actions \
  -H "X-NullSpend-Key: ns_live_sk_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "support-bot",
    "actionType": "send_email",
    "payload": {"to":"customer@example.com","subject":"Your refund has been processed"},
    "expiresInSeconds": 86400
  }'

Response

201 Created:

{
  "id": "ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "pending",
  "expiresAt": "2026-03-21T14:30:00.000Z"
}

Rate limit headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset

Side effect: sends a Slack notification (if configured) to alert the human approver.

Errors

CodeHTTPWhen
validation_error400Invalid action type, payload too large, nesting too deep
invalid_json400Malformed JSON body
unsupported_media_type415Content-Type is not application/json
payload_too_large413Body exceeds 1 MB
authentication_required401Missing or invalid API key
rate_limit_exceeded429Per-key rate limit exceeded

Get Action

GET /api/actions/:id

Retrieve a single action by ID. Agents can poll this endpoint to check if their action has been approved.

Authentication

Dual (API key or session)

Parameters

NameInTypeRequiredDescription
idpathstringYesAction ID (ns_act_*).

Request

// Agent polling for approval
const res = await fetch(
  "https://nullspend.dev/api/actions/ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  { headers: { "X-NullSpend-Key": "ns_live_sk_abc123..." } }
);
import requests

resp = requests.get(
    "https://nullspend.dev/api/actions/ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    headers={"X-NullSpend-Key": "ns_live_sk_abc123..."},
)
curl https://nullspend.dev/api/actions/ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "X-NullSpend-Key: ns_live_sk_abc123..."

Response

200 OK:

{
  "id": "ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "agentId": "support-bot",
  "actionType": "send_email",
  "status": "approved",
  "payload": {
    "to": "customer@example.com",
    "subject": "Your refund has been processed"
  },
  "metadata": { "ticketId": "T-1234" },
  "createdAt": "2026-03-20T14:30:00.000Z",
  "approvedAt": "2026-03-20T14:35:00.000Z",
  "rejectedAt": null,
  "executedAt": null,
  "expiresAt": "2026-03-21T14:30:00.000Z",
  "expiredAt": null,
  "approvedBy": "ns_usr_aabbccdd-eeff-0011-2233-445566778899",
  "rejectedBy": null,
  "result": null,
  "errorMessage": null,
  "environment": null,
  "sourceFramework": null
}

Errors

CodeHTTPWhen
authentication_required401Invalid API key or no session
not_found404Action not found or not owned by user
rate_limit_exceeded429Per-key rate limit (API key auth only)

Approve Action

POST /api/actions/:id/approve

Approve a pending action. Only a human in the dashboard can approve — this endpoint requires session auth.

Authentication

Session (dashboard)

Parameters

NameInTypeRequiredDescription
idpathstringYesAction ID (ns_act_*).

No request body required.

Request

# Requires dashboard session cookie
curl -X POST https://nullspend.dev/api/actions/ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890/approve \
  -H "Cookie: session=..."

Response

200 OK:

{
  "id": "ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "approved",
  "approvedAt": "2026-03-20T14:35:00.000Z"
}

State Transitions

Only valid from pending status. The action must not have expired.

Errors

CodeHTTPWhen
authentication_required401No valid session
not_found404Action not found or not owned by user
invalid_action_transition409Action not in pending status
action_expired409Action has expired
stale_action409Concurrent modification detected

Reject Action

POST /api/actions/:id/reject

Reject a pending action. Only a human in the dashboard can reject — this endpoint requires session auth.

Authentication

Session (dashboard)

Parameters

NameInTypeRequiredDescription
idpathstringYesAction ID (ns_act_*).

No request body required.

Request

# Requires dashboard session cookie
curl -X POST https://nullspend.dev/api/actions/ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890/reject \
  -H "Cookie: session=..."

Response

200 OK:

{
  "id": "ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "rejected",
  "rejectedAt": "2026-03-20T14:35:00.000Z"
}

State Transitions

Only valid from pending status. The action must not have expired.

Errors

CodeHTTPWhen
authentication_required401No valid session
not_found404Action not found or not owned by user
invalid_action_transition409Action not in pending status
action_expired409Action has expired
stale_action409Concurrent modification detected

Mark Action Result

POST /api/actions/:id/result

Report the outcome of an approved action. Called by the agent after executing (or failing to execute) the action.

Authentication

API key

Parameters

NameInTypeRequiredDescription
idpathstringYesAction ID (ns_act_*).
statusbodystringYes"executing", "executed", or "failed".
resultbodyobjectNoExecution result. Max 64 KB, max 20 nesting levels. Forbidden when status is "executing".
errorMessagebodystringNoError description. Max 4,000 chars. Required when status is "failed". Forbidden when status is "executing" or "executed".
Idempotency-KeyheaderstringNoDeduplication key for idempotent retries.

Request

// Mark as executing
await fetch(
  "https://nullspend.dev/api/actions/ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890/result",
  {
    method: "POST",
    headers: {
      "X-NullSpend-Key": "ns_live_sk_abc123...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ status: "executing" }),
  }
);

// Mark as executed with result
await fetch(
  "https://nullspend.dev/api/actions/ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890/result",
  {
    method: "POST",
    headers: {
      "X-NullSpend-Key": "ns_live_sk_abc123...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      status: "executed",
      result: { emailId: "msg_abc123", sentAt: "2026-03-20T14:36:00.000Z" },
    }),
  }
);
import requests

# Mark as failed
resp = requests.post(
    "https://nullspend.dev/api/actions/ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890/result",
    headers={"X-NullSpend-Key": "ns_live_sk_abc123..."},
    json={
        "status": "failed",
        "errorMessage": "SMTP connection refused: relay.example.com:587",
    },
)
curl -X POST https://nullspend.dev/api/actions/ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890/result \
  -H "X-NullSpend-Key: ns_live_sk_abc123..." \
  -H "Content-Type: application/json" \
  -d '{"status": "executed", "result": {"emailId": "msg_abc123"}}'

Response

200 OK:

{
  "id": "ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "executed",
  "executedAt": "2026-03-20T14:36:00.000Z"
}

Rate limit headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset

State Transitions

pending → approved → executing → executed
                                ↘ failed
  • approvedexecuting
  • executingexecuted or failed
  • Terminal states (executed, failed, rejected, expired) cannot transition further.

Errors

CodeHTTPWhen
validation_error400Invalid status, missing errorMessage for failed, result set for executing
invalid_json400Malformed JSON body
unsupported_media_type415Content-Type is not application/json
payload_too_large413Body exceeds 1 MB
authentication_required401Missing or invalid API key
not_found404Action not found or not owned by user
invalid_action_transition409Action in a terminal state
stale_action409Concurrent modification detected
rate_limit_exceeded429Per-key rate limit exceeded

Get Action Costs

GET /api/actions/:id/costs

Retrieve cost events associated with an action.

Authentication

Dual (API key or session)

Parameters

NameInTypeRequiredDescription
idpathstringYesAction ID (ns_act_*).

Request

const res = await fetch(
  "https://nullspend.dev/api/actions/ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890/costs",
  { headers: { "X-NullSpend-Key": "ns_live_sk_abc123..." } }
);
import requests

resp = requests.get(
    "https://nullspend.dev/api/actions/ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890/costs",
    headers={"X-NullSpend-Key": "ns_live_sk_abc123..."},
)
curl https://nullspend.dev/api/actions/ns_act_a1b2c3d4-e5f6-7890-abcd-ef1234567890/costs \
  -H "X-NullSpend-Key: ns_live_sk_abc123..."

Response

200 OK:

{
  "data": [
    {
      "id": "ns_evt_b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "provider": "openai",
      "model": "gpt-4o",
      "inputTokens": 1500,
      "outputTokens": 500,
      "cachedInputTokens": 0,
      "reasoningTokens": 0,
      "costMicrodollars": 6750,
      "tags": { "agent": "support-bot" },
      "createdAt": "2026-03-20T14:35:30.000Z"
    }
  ]
}

Errors

CodeHTTPWhen
authentication_required401Invalid API key or no session
not_found404Action not found or not owned by user
rate_limit_exceeded429Per-key rate limit (API key auth only)

On this page