API reference

The request contract

Path shape, response envelope, pagination, batching and the endpoints that describe the catalogue to your code at runtime.

Base

Base URL and method

monocrawl.com/v1

Base URL

GET

Method

x-api-key

Auth

JSON

Encoding

Data endpoints are GET with query-string parameters. The batch transport is POST /v1/batch. Responses are JSON by default. search/all and search/creators can instead stream SSE with stream=1; see streaming.

Paths

How a path resolves

The first segment after /v1 is the platform; everything after it is the endpoint id. Some endpoints take their identifier in the path rather than the query string, and the router recognises both call styles.

FormExampleResolves as
/v1/{platform}/{endpoint}/v1/github/profileThe ordinary form. Parameters go in the query string.
/v1/{platform}/{a}/{b}/v1/github/repo/issuesEndpoint ids may span several segments; everything after the platform is the endpoint id.
/v1/{platform}/v1/statusA bare platform resolves to that platform’s index endpoint.
/v1/{platform}/{collection}/{id}/v1/web/jobs/job_9f3aA trailing identifier resolves to the collection’s get endpoint, with the id passed as its declared parameter.
/v1/{platform}/{id}/{sub}/v1/monitors/mon_1b2c/runsAn identifier in the middle is lifted out and the surrounding words resolve the endpoint.

A path segment is treated as an identifier rather than a route word when it contains a digit or an underscore, or runs past 24 characters — so job_9f3a and 12345 are ids while runs and checks are route words. An unknown path answers 404 ENDPOINT_NOT_FOUND.

Envelope

One success shape, one error shape

Both shapes always carry credits_used and request_id, for standard routed JSON responses. Error responses include credits_remaining only when the balance is known. Batch has a separate wrapper and per-item envelopes; streamed searches carry an envelope in their final result event.

Success · 200

success envelope
{
  "success": true,
  "platform": "github",
  "endpoint": "/v1/github/profile",
  "data": { … },
  "credits_used": 1,
  "credits_remaining": 99,
  "request_id": "req_4f2b8c1d09ae37b562",
  "cached": false
}

Error · any non-2xx

error envelope
{
  "success": false,
  "error": {
    "type": "RESOURCE_NOT_FOUND",
    "message": "GitHub returned 404 for this resource.",
    "status": 404,
    "doc_url": "https://www.monocrawl.com/docs/errors#resource-not-found"
  },
  "credits_used": 0,
  "credits_remaining": 99,
  "request_id": "req_71ac0e5b39d8f2c604"
}

data

The endpoint-specific payload. Many lists use { items, count, cursor }; bundles and service operations can have named collections or other structures. Consult the endpoint description.

synthetic / degraded

Sandbox-only compatibility flags. Production /v1 returns live/cache data or an error and never substitutes representative output after upstream failure.

_warnings

Optional explanatory notes inside data. Unavailable fields can be null or absent; a warning is not guaranteed for every missing field.

error.doc_url points at the exact section of the errors page for that error type — a client can surface it verbatim.

Pagination

Cursors, not page maths

Many list endpoints return items, count and cursor inside data. Where supported, pass the cursor back with the original filters. A null cursor means no usable continuation was supplied, not proof that all history was retrieved. Cursors are opaque — some are page numbers, some are upstream tokens — so echo the value you were given rather than deriving the next one.

pagination
$ curl "https://www.monocrawl.com/v1/github/profile/repos?handle=torvalds&limit=50" \
    -H "x-api-key: mn_your_key_here"

"data": {
  "items": [ … 50 rows … ],
  "count": 50,
  "cursor": "2"
}

# continue with the cursor exactly as it was given to you
$ curl ".../v1/github/profile/repos?handle=torvalds&limit=50&cursor=2" -H "x-api-key: …"

Page-size parameters, defaults and caps vary by endpoint. Some routes use pageor do not support continuation. Each page is a separate request under that endpoint’s billing rules; a larger requested limit is not a guarantee of more results or a lower price.

Batch

POST /v1/batch — up to 20 calls at once

The batch endpoint takes a list of ordinary requests and runs each one through the full pipeline independently. Items may target different platforms. The wrapper itself costs nothing — you pay only for the calls inside it.

request
curl -X POST "https://www.monocrawl.com/v1/batch" \
  -H "x-api-key: mn_your_key_here" \
  -H "content-type: application/json" \
  -d '{
    "requests": [
      { "platform": "github",  "endpoint": "profile", "params": { "handle": "torvalds" } },
      { "platform": "bluesky", "endpoint": "profile", "params": { "handle": "bsky.app" } }
    ]
  }'
response
{
  "success": true,
  "count": 2,
  "succeeded": 2,
  "failed": 0,
  "credits_used": 2,
  "credits_remaining": 97,
  "results": [
    { "index": 0, "status": 200, "success": true, "platform": "github",
      "endpoint": "/v1/github/profile", "data": { … }, "credits_used": 1,
      "credits_remaining": 98, "request_id": "req_…", "cached": false },
    { "index": 1, "status": 200, "success": true, "platform": "bluesky",
      "endpoint": "/v1/bluesky/profile", "data": { … }, "credits_used": 1,
      "credits_remaining": 97, "request_id": "req_…", "cached": false }
  ]
}

Twenty items maximum

A larger list is rejected with 400 INVALID_PARAMETERS and details.max, before anything is charged. Split bigger jobs client-side.

Items fail independently

An upstream failure or an out-of-credits item degrades that item only. Its entry carries its own status and error; its siblings still return data.

Header auth only

Batch reads only x-api-key, not Authorization: Bearer. Query-string API keys are not supported on batch or GET routes.

Check each item

The wrapper’s success is true whenever the batch itself was accepted. Read succeeded, failed, and each item’s own status and success.

The top-level credits_used is the sum across items; credits_remaining, when present, is the lowest balance any item observed while the batch ran. For an exact figure afterwards, call /v1/credits/balance. Batch items are admitted individually, so each one counts against the per-key rate limit. The wrapper does not return per-item HTTP headers or a top-level request_id. Idempotency-Key and Cache-Control headers are not forwarded to items.

Discovery

Read the catalogue at runtime

These endpoints describe the API to a client without any hard-coded list. They all cost 0 credits, so a service can refresh its view of the catalogue on every boot.

EndpointParametersReturns
GET /v1/utility/endpointsplatform, search, methodEvery active endpoint with its id, name, credit_cost, params and description. The authoritative catalogue.
GET /v1/utility/endpointid or urlOne endpoint in full, including a ready-to-run curl with its required parameters filled from examples.
GET /v1/utility/quickstartplatformA low-cost registered data endpoint (for a platform, or overall) with its curl and the envelope it will return.
GET /v1/utility/llmsplatform, formatThe whole catalogue as one payload for an agent: format=markdown (default) or format=json.
GET /v1/credits/balancecredits_remaining, credits_lifetime and when the balance last changed.
GET /v1/credits/transactionslimit, cursor, request_idYour credit ledger, newest first — every debit and credit with the request_id that caused it.
GET /v1/statusPer-platform requests, error_rate, p50/p95 latency and state over the last 15 minutes.

utility/llms is the fastest way to teach an agent the whole surface: one call returns the base URL, the auth header, both envelope shapes, the error type list and every endpoint with its cost and parameters.

For humans, the browsable endpoint reference lists every operation with its parameters and a ready-to-run curl. For tooling, /openapi.json (or /openapi.yaml) is the same registry as an OpenAPI 3.1 document — import it into Postman, Insomnia or a client generator. These surfaces share the registry but expose different subsets and metadata; registration and recorded proof are not a guarantee of current availability.

Headers

What comes back on the wire

HeaderWhenMeaning
retry-after429 or 503, when providedSeconds to wait before retrying. Per-key rate-limit refusals also include error.details.retry_after_seconds; other refusals may omit that field.
content-typewhen suppliedapplication/json by default; streamed searches use text/event-stream.
x-request-idwhen suppliedSame value as request_id in the body. Quote it when contacting support.
x-credits-usedwhen suppliedCredits charged for this request — 0 on any failure.
x-credits-remainingwhen suppliedYour balance after the request.
x-cachewhen suppliedHIT or MISS on successful routed responses. A cache hit costs 0 credits — see endpoint pricing.
x-idempotent-replayreplayed requeststrue when the response came from an earlier identical call rather than a new one. See below.
x-ratelimit-limit · x-ratelimit-remaining · x-ratelimit-resetrouted, rate-admitted JSON responsesThe request-window cap, what is left of it in the current sliding window (this request included) and the Unix second at which its oldest entry ages out. See rate limits.
x-concurrency-limit · x-concurrency-remainingrouted, rate-admitted JSON responsesThe in-flight cap and the slots still free as this request entered. Not an alias of the request-window headers: a client that conflates the two throttles against the wrong ceiling.
deprecation · sunset · linkdeprecated endpoints onlyThe endpoint is scheduled for removal: when that was announced, when it happens, and where to read about it. Formats and the notice period are on versioning.

Early authentication refusals, batch wrappers, SSE and idempotent replays may omit some headers. Rate and concurrency headroom are not duplicated in the body. Read credits_remaining when present, or call /v1/credits/balance for free.

Idempotency

Retry a lost request without paying twice

If a response is lost — a timeout, a dropped connection, a crash between the call and reading it — you cannot tell whether the request landed. Retrying blindly risks paying twice for one result. Send an Idempotency-Key header on the first direct GET request and reuse it on retries. A stored replay adds no charge. This header does not protect batch items or MCP calls. If the idempotency store is unavailable, the call proceeds without replay protection and returnsx-idempotency-status: unavailable; do not assume an automatic retry is deduplicated.

the same call, twice, charged once
# first call: runs, and is charged
$ curl "https://www.monocrawl.com/v1/github/profile?handle=torvalds" \
    -H "x-api-key: mn_your_key_here" \
    -H "Idempotency-Key: 8f3a1c2e-order-4471"

x-credits-used: 1
{ "success": true, "data": { … }, "credits_used": 1, "request_id": "req_4f2b8c…" }

# the response was lost — retry with the SAME key
$ curl "https://www.monocrawl.com/v1/github/profile?handle=torvalds" \
    -H "x-api-key: mn_your_key_here" \
    -H "Idempotency-Key: 8f3a1c2e-order-4471"

x-idempotent-replay: true
{ "success": true, "data": { … }, "credits_used": 1, "request_id": "req_4f2b8c…" }
# ^ the stored original result. No new charge; its balance is historical.

The replay preserves the result

You get the stored original HTTP status and JSON body, with x-idempotent-replay: true. JSON formatting and the original response headers are not preserved. No new upstream call is made and no usage event is recorded.

It costs nothing

The original charge stands; the replay adds zero. The body still shows what the first call cost, because it IS the first call’s response.

Keys are yours alone

Scoped to your account, so your keys can never collide with another customer’s. Any string up to 255 characters — a UUID per logical operation works well.

Records last 24 hours

After that the key is free to reuse. Reusing one sooner for a DIFFERENT request returns 422 rather than the wrong body.

If a second request arrives while the first is still in flight, the second gets 409 with details.reason = idempotency_in_progress — wait for the first rather than racing it. The header is entirely optional; without it nothing about a request changes.

Webhooks

Signed events pushed to your server

Register an https URL in the console (Dashboard → API → Integrations) and Monocrawl POSTs you a signed JSON event when something happens on your account — an async job finishing, a failed job and its refund status, a low balance or an auto-recharge outcome. Up to five endpoints per account, each with its own signing secret and its own event filter.

what a delivery looks like
POST https://example.com/monocrawl/webhook
content-type: application/json
monocrawl-event: job.completed
monocrawl-delivery: whd_8c1d09ae37b562
monocrawl-signature: t=1767225600,v1=5f8a…e2c1

{
  "id": "whd_8c1d09ae37b562",
  "type": "job.completed",
  "created_at": "2026-01-01T00:00:00.000Z",
  "data": { "job_id": "job_9f3a", "kind": "crawl", "credits_charged": 40 }
}

Verify the signature

Every delivery carries monocrawl-signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 over `${t}.${rawBody}` with your endpoint's secret. Verify before trusting anything in the body — anyone can POST to a public URL, a valid signature proves possession of the signing secret. Keep that secret private.

node — reject anything unsigned
import crypto from 'node:crypto';

// rawBody is the original Buffer (or unmodified UTF-8 string), BEFORE JSON parsing.
export function verifyMonocrawlSignature(rawBody, header, secret, nowMs = Date.now()) {
  const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? '');
  if (!match || !secret) return false;
  const [, timestamp, signature] = match;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) || Math.abs(nowMs / 1000 - seconds) > 300) return false;
  const expected = crypto.createHmac('sha256', secret)
    .update(timestamp + '.').update(rawBody).digest();
  const received = Buffer.from(signature, 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(expected, received);
}

Event types

EventFires when
billing.low_balanceBalance crossed the configured alert threshold; by default, one fifth of the last purchased pack.
billing.credits_exhaustedBalance reached zero. Paid requests requiring more credits return 402; free endpoints remain usable. Emitted once per exhaustion episode.
billing.purchase_succeededA pack purchase settled and its credits were granted.
billing.auto_recharge.succeededAn enabled auto-recharge charged the saved card and granted credits.
billing.auto_recharge.failedAuto-recharge could not charge the card; check billing or top up manually.
subscription.renewedLegacy subscriptions only: a period renewed. Current offers are one-time packs, not subscriptions.
subscription.endedLegacy subscriptions only: the subscription ended and its remaining plan credits expired.
job.completedAn async job finished. Use data.job_id to retrieve its result.
job.failedAn async job failed. Inspect credits_refunded and refund_status: a refund marked due is pending, not yet credited.
monitor.findingsA monitor recorded findings for delivery to its configured destinations.
monitor.attentionA monitor needs attention, for example an ambiguous subject or repeated failed checks.
monitor.digestA scheduled monitor summary is ready for delivery.
test.pingA test delivery requested from the dashboard.

Answer 2xx fast

Anything else — including a timeout past 10 seconds — counts as a failed attempt. Acknowledge first, process after; redirects are never followed.

Five delayed retries

After successive failures the delays are 1 min, 5 min, 30 min, 2 h and 12 h: about 14 h 36 min in total, plus processing and worker scheduling time. After the schedule is exhausted the delivery is marked failed and stays in your history.

Dedupe on the delivery id

Delivery is at-least-once: if your 2xx is lost on the wire, the same id can arrive again. Treat the id as the idempotency key on your side.

Dead endpoints turn themselves off

Ten consecutive failed deliveries auto-disable the endpoint (the reason is shown in the console). Fix the receiver, then re-enable — that resets the count.

Rotating a secret takes effect immediately — deliveries signed with the old secret stop verifying at that moment, so rotate when your receiver is ready for the new one. The "Test" button in the console sends a test.ping synchronously and shows you exactly what your endpoint answered.

First call in under a minute

150 free credits and a ready-made key the moment you sign up. No card.