Askfirst

Docs

Everything is one resource: an approval request. It starts pending and ends approved, denied, expired or cancelled.

1. Get a key

Sign in with your email and create an API key in the dashboard. Send it as Authorization: Bearer ak_....

2a. MCP (Claude Code, Cursor, any MCP client)

claude mcp add --transport http askfirst https://getaskfirst.com/mcp --header "Authorization: Bearer ak_..."

Tools: request_approval (title, details, approvers, wait_seconds, expires_in) and wait_for_approval (id, wait_seconds). Each call waits up to 55 seconds; if the human hasn't decided yet the agent is told to call wait_for_approval again.

Other clients: server URL https://getaskfirst.com/mcp, transport Streamable HTTP, the same Authorization header.

2b. REST API

Create

POST https://getaskfirst.com/v1/approvals
{
  "title": "Delete 1,204 stale rows from prod.users",          // required, ≤200 chars
  "details": "Rows where last_login < 2023-01-01 ...",       // optional, ≤10,000 chars
  "approvers": ["ops@example.com"],                        // optional; verified approvers, default: you
  "expires_in": 3600,                                      // optional seconds, 60–604800, default 86400
  "callback_url": "https://example.com/hooks/askfirst",    // optional, public https
  "metadata": {"run_id": "r_42"}                           // optional, ≤4 KB
}

JSON or form-encoded bodies both work. Returns the approval object with status 201.

Read, wait, list, cancel

GET  https://getaskfirst.com/v1/approvals/:id
GET  https://getaskfirst.com/v1/approvals/:id/wait?timeout=55     # long-poll; returns as soon as it's decided
GET  https://getaskfirst.com/v1/approvals?status=pending&limit=20&before=apr_...
POST https://getaskfirst.com/v1/approvals/:id/cancel
GET  https://getaskfirst.com/v1/account                            # plan and this month's usage

Approval object

{"id":"apr_...","object":"approval","status":"approved","title":"...","details":"...",
 "approvers":["you@example.com"],"decided_by":"you@example.com","comment":"ok, but only this once",
 "callback_url":null,"metadata":{},"created_at":"2026-10-23T17:02:11.000Z",
 "expires_at":"2026-10-24T17:02:11.000Z","decided_at":"2026-10-23T17:03:40.000Z"}

Errors

Errors return {"error":{"code":"...","message":"..."}}. 401 bad key, 400 invalid input, 402 plan limit reached, 404 not found, 409 not pending.

Webhooks

When a request with a callback_url reaches a final status we POST {"type":"approval.approved","data":{...approval}}. Failed deliveries retry for about 15 hours. Verify the Askfirst-Signature header, t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 of t + "." + rawBody keyed with your webhook secret from the dashboard:

import crypto from "node:crypto";
const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
const ok = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex") === v1
  && Math.abs(Date.now() / 1000 - Number(t)) < 300;

Python

import requests, os
H = {"Authorization": f"Bearer {os.environ['ASKFIRST_KEY']}"}
a = requests.post("https://getaskfirst.com/v1/approvals", headers=H, json={"title": "Send invoice #88 to ACME"}).json()
while a["status"] == "pending":
    a = requests.get(f"https://getaskfirst.com/v1/approvals/{a['id']}/wait?timeout=55", headers=H).json()
if a["status"] != "approved":
    raise SystemExit(f"not approved: {a['status']}")

Approvers

Your own email is always an approver. On Pro and Team you can invite more from the dashboard; each one accepts by email before they can receive requests. Any one approver's decision settles the request.