Buying results

Execute

Run a tool on one input or a batch, and pay only for items that pass the published check.

POST /v1/execute buys results. You name a tool and send one input or a batch. Arettic checks the input for free, holds the price, calls the provider with its own credentials, runs the published pass rule on the answer, and settles: a pass is charged, a fail is refunded and its data withheld. Every live request ends in one immutable receipt. This page has every field, every status and every code, with examples in curl, TypeScript, Python and MCP.

Arettic is pre-launch. Keys go to design partners first; everyone else joins the waitlist. The @arettic/sdk and @arettic/mcp packages (npm) and the arettic package (PyPI) are published at launch. With a test key (sk_test_…) everything on this page works against mock providers and nothing is charged; see Test keys.

The request

Send a JSON object with your agent key in Authorization: Bearer sk_…. The body must be under 1 MB. Fields not listed here are ignored.

Execute request fields
FieldTypeMeaning
tool_idstring, requiredThe tool's id from recommend or GET /v1/tools (its slug, for example hunter-verify-email), or its UUID. A live key can't see mock- tools; a test key runs the mock provider for the tool's task type.
inputobjectOne input. Its fields depend on the tool's task type; see the table below.
inputsarray of 1 to 1,000 objectsA batch. Send input or inputs, not both. Up to 25 items run at once and come back in the same answer. More than 25 run as a job and the answer is queued. See Batches.
max_pricewhole number of credits, as a JSON number or a string of digitsThe most this whole request may cost. Price per item × items must be at or under it, or nothing runs and the answer is price_above_max. It also caps any fallback. See max_price.
idempotency_keystring, 1 to 200 charactersMakes a retry safe: the same agent sending the same key gets the first answer back and is never charged twice. See Idempotency.
approval_idapproval UUIDFrom an approval_required answer, once an owner has approved it. The request must be exactly the approved one. See Approvals.
fallbackboolean, default falseIf an item fails its check or the provider errors, try the next-ranked tool for the task once, within max_price. Also lets a paused tool be replaced before the call. See Fallback.

The input fields come from the tool's task type. Every field is checked before any provider is called, and a request that fails the check costs nothing. The check is the same for live and test keys, except that test keys skip the DNS lookups (mock tools use made-up domains). Every problem is reported at once, the first ten in the message, in the form input.field: problem or inputs[3].field: problem, ending with Nothing was charged. If Arettic's own DNS can't answer, the request is not blocked.

Input fields and the free pre-call check, per task type
task_typeInput fieldsChecked before the call
find_emailfirst_name: string; last_name: string; domain: company domain, e.g. acme.comfirst_name and last_name up to 100 characters. domain must look like a domain and must exist in DNS.
verify_emailemail: stringemail must be an email address of up to 254 characters, and its domain must exist in DNS.
enrich_companydomain: company domain; name: company name (if no domain); country: optional, with namedomain must look like a domain, or send name (up to 200 characters) instead. A domain must exist in DNS.
enrich_personfirst_name: string; last_name: string; company_domain: company domainfirst_name and last_name up to 100 characters. company_domain must look like a domain and must exist in DNS.
web_searchquery: string, up to 500 characters; n: 1–25, default 5query up to 500 characters. n, if sent, is a whole number from 1 to 25; the default is 5.
extract_urlurl: http(s) URLurl must be a full http or https URL to a public host. Localhost, private and link-local addresses are refused.

The JSON Schemas for the request and the response are at https://arettic.com/schemas. The full pass rule of each task type, with its output fields, is on Pass rules.

What happens to a request

Every live request goes through the same steps, in this order. Nothing is held until step 4, so anything refused before that is free.

  1. Validate (free). The tool must exist, be purchasable and not paused; input or inputs must be present and within the limits; every input passes the syntax check, then the DNS and public-host checks. A problem answers invalid_input, unknown_tool, not_purchasable or tool_paused, and no provider is called.
  2. Price (free). The price per item is the tool's price for your plan (pay as you go pays the listed price per success; Pro and Max pay their multiplier). price × items is the most the request can cost. If it is over max_price, the answer is price_above_max.
  3. Replay. With an idempotency_key that this agent already used for the same request, the first answer comes back with replayed: true. Nothing runs and nothing is charged.
  4. Authorize. The org's balance must cover price × items, or the answer is insufficient_credits. An org that has never topped up can hold at most 50 trial credits an hour, or the answer is trial_limit. Then, unless the request carries an approval_id, the amount is checked against the agent's approval threshold and its monthly budget: over either, the answer is approval_required (HTTP 202) and an owner is emailed. The agent can't change any of these limits.
  5. Hold. In one transaction under the org's and the agent's row locks: the budget is checked again (two parallel requests can't overshoot it; the one that would answers approval_required with over_budget), the provider's per-org daily quota and the acceptable-use rule are applied, and the price is held for every item, trial credits first, then paid. A refusal here (provider_quota, aup_limit, credits_frozen) holds nothing. Over 25 items, the whole hold is placed now and the request becomes a job.
  6. Call. Up to 4 items run at a time (8 in a job). Arettic calls the provider with its own credentials. Each attempt is cut off after 30 seconds, and there is one retry after an error or a timeout. A provider that answers 404 or 422 has no record: that counts as an empty answer, not an error.
  7. Check. The published pass rule for the task type runs on the answer and gives pass, partial (web search only) or fail with a reason. For find_email, Arettic first verifies the address with its own verifier and the rule judges that verdict, so a catch-all never passes. The rule's version is check_version (v1), on the answer and on the receipt. If the checker itself throws, the item fails with check_error: you never pay for Arettic's bug.
  8. Settle. Straight after the check. A pass captures the price. A partial captures price × fraction, rounded up, and releases the rest. A fail, a provider error or a timeout releases everything. Every item ends captured or released; an item caught mid-call by a crash is released within a few minutes with the reason interrupted.
  9. Deliver. A passed or partial item comes back with its result. A failed item comes back with its reason only; the paid data is withheld. The inputs and results are stored encrypted for 7 days, so a replay can return them; the receipt keeps only hashes after that.

Examples

One input with a price cap and an idempotency key, with a live key. Use a tool_id your own recommend call returned; hunter-verify-email and its 7-credit price are an example.

curl
curl https://api.arettic.com/v1/execute \
  -H "Authorization: Bearer $ARETTIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tool_id": "hunter-verify-email",
    "input": { "email": "[email protected]" },
    "max_price": 10,
    "idempotency_key": "verify-jason-1"
  }'
TypeScript
import { Arettic, AretticApiError } from "@arettic/sdk";

// Reads ARETTIC_API_KEY (and ARETTIC_API_URL) from the environment when you leave them out.
const arettic = new Arettic({ apiKey: process.env.ARETTIC_API_KEY });

try {
  const bought = await arettic.execute(
    { tool_id: "hunter-verify-email", input: { email: "[email protected]" }, max_price: 10 },
    { idempotencyKey: "verify-jason-1" }, // else the SDK makes a UUID for this call
  );
  switch (bought.status) {
    case "passed":
    case "partial":
      console.log(bought.result, bought.charged.credits); // "7"
      break;
    case "failed":
      console.log("not charged:", bought.reason);
      break;
    case "approval_required":
      console.log("waiting for an owner:", bought.approval_id);
      break;
    case "queued": // only when inputs has more than 25 items
      console.log((await arettic.waitForJob(bought.job_id)).summary);
      break;
  }
} catch (err) {
  // Anything the API refused: err.code is a code from /docs/errors, e.g. "price_above_max".
  if (err instanceof AretticApiError) console.error(err.status, err.code, err.message);
  else throw err;
}
Python
from arettic import Arettic, AretticApiError

client = Arettic()  # reads ARETTIC_API_KEY (and ARETTIC_API_URL, if set)

try:
    bought = client.execute(
        {
            "tool_id": "hunter-verify-email",
            "input": {"email": "[email protected]"},
            "max_price": 10,
        },
        idempotency_key="verify-jason-1",  # else the SDK makes a UUID for this call
    )
except AretticApiError as err:
    # Anything the API refused: err.code is a code from /docs/errors, e.g. "price_above_max".
    print(err.status, err.code, err.message)
    raise

status = bought["status"]
if status in ("passed", "partial"):
    print(bought["result"], bought["charged"]["credits"])  # "7"
elif status == "failed":
    print("not charged:", bought["reason"])
elif status == "approval_required":
    print("waiting for an owner:", bought["approval_id"])
elif status == "queued":  # only when inputs has more than 25 items
    print(client.wait_for_job(bought["job_id"])["summary"])

Over MCP the execute tool takes the same fields as top-level arguments (max_price as a number). The tool result is the same JSON as text content; a refusal comes back as a tool error whose text is the error envelope. Client configs are on MCP server.

MCP tool call
{
  "name": "execute",
  "arguments": {
    "tool_id": "hunter-verify-email",
    "input": { "email": "[email protected]" },
    "max_price": 10,
    "idempotency_key": "verify-jason-1"
  }
}
Response (example)
{
  "status": "passed",
  "execution_id": "c02576cf-b3ce-4b0f-a30c-6e8aad4328ce",
  "receipt_id": "67a1fb46-c554-4cf6-b4e2-df1dbdf0e936",
  "tool_id": "hunter-verify-email",
  "task_type": "verify_email",
  "check_version": "v1",
  "charged": { "credits": "7", "usd": "0.007" },
  "refunded": { "credits": "0", "usd": "0.000" },
  "result": { "email": "[email protected]", "status": "valid" }
}

Every response status

Read status first. Money is always { "credits": "7", "usd": "0.007" }: credits as a whole number in a string (1 credit is $0.001) and the same amount in dollars with three decimals.

Execute statuses
statusHTTPWhenWhat comes with it
passed200One input, and the result passed the check.result, charged (the price), refunded (0), execution_id, receipt_id.
partial200One input for web_search, and fewer results than asked for.result, reason (results:2/5), charged pro rata, refunded the rest.
failed200One input, and the check failed, the provider errored or timed out.reason only: no result, charged is 0, refunded is the full price. (Under per-call pricing a failed check is charged and keeps its result.)
completed200inputs with up to 25 items, all run and settled.summary and items[], one per input in order, each with its own status. charged and refunded are the totals.
queued202inputs with more than 25 items, with a live key.job_id, items, max_charge, poll, message. Poll GET /v1/jobs/{job_id}.
approval_required202The amount is over the agent's approval threshold or its monthly budget.approval_id, reason (over_threshold or over_budget), amount, expires_at, message. Nothing ran.
declined402, 403, 409, 410 or 429The org can't pay, the trial limit or a quota is hit, or an approval can't be used.An error next to it with the code. Nothing ran and nothing is held. See Errors.

A failed check or a provider error is not an HTTP error. The answer is 200 with status: "failed" and a reason, and nothing is charged. Only refusals use the error envelope.

failed

Response (example)
{
  "status": "failed",
  "execution_id": "9b1f0d2e-4c6a-4e8b-9f3d-2a7c5e1b8d40",
  "receipt_id": "0d2e7f31-8a4b-4c9d-b1e6-5f7a9c3d2e10",
  "tool_id": "hunter-verify-email",
  "task_type": "verify_email",
  "check_version": "v1",
  "charged": { "credits": "0", "usd": "0.000" },
  "refunded": { "credits": "7", "usd": "0.007" },
  "reason": "status:unknown"
}

partial (pro rata)

Only web_search can be partial. You ask for n results (default 5). If the provider returns at least n valid, distinct URLs the item passes. If it returns some but fewer, you get them all and pay price × found ÷ n, rounded up to a whole credit. No usable URL is a fail with no_result. Two of five results on a 10-credit tool costs 4 credits:

Response (example)
{
  "status": "partial",
  "execution_id": "5e8c1a7b-2d3f-4a6e-8b9c-1f2e3d4c5b6a",
  "receipt_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "tool_id": "exa-web-search",
  "task_type": "web_search",
  "check_version": "v1",
  "charged": { "credits": "4", "usd": "0.004" },
  "refunded": { "credits": "6", "usd": "0.006" },
  "result": {
    "results": [
      { "url": "https://example.com/saas-directory", "title": "US SaaS companies", "snippet": "A directory of…" },
      { "url": "https://acme.com/blog/saas-in-the-us", "title": "SaaS in the US", "snippet": "The market…" }
    ]
  },
  "reason": "results:2/5"
}

completed (a batch of 25 or fewer)

Each item is held, called, checked and settled on its own, so one bad input never affects the others. items[] keeps the order of inputs; each entry has index, status, charged, and result or reason. The per-item execution_id and refund are on the receipt. Three verify-email inputs, one of them failing:

Response (example)
{
  "status": "completed",
  "receipt_id": "7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
  "tool_id": "hunter-verify-email",
  "task_type": "verify_email",
  "check_version": "v1",
  "charged": { "credits": "14", "usd": "0.014" },
  "refunded": { "credits": "7", "usd": "0.007" },
  "summary": { "items": 3, "passed": 2, "partial": 0, "failed": 1 },
  "items": [
    {
      "index": 0,
      "status": "passed",
      "charged": { "credits": "7", "usd": "0.007" },
      "result": { "email": "[email protected]", "status": "valid" }
    },
    {
      "index": 1,
      "status": "failed",
      "charged": { "credits": "0", "usd": "0.000" },
      "reason": "status:unknown"
    },
    {
      "index": 2,
      "status": "passed",
      "charged": { "credits": "7", "usd": "0.007" },
      "result": { "email": "[email protected]", "status": "valid" }
    }
  ]
}

queued (a batch of more than 25)

The whole hold (max_charge) is reserved now, and a worker runs the items. Poll the job; once it has its receipt the job answer carries the same summary and items[] as a completed batch. Details, states and the job.completed webhook are on Batches and jobs.

Response (example)
{
  "status": "queued",
  "job_id": "3f9c2c1e-6d7a-4b8f-a2e1-9c0b5d4e3f21",
  "items": 30,
  "max_charge": { "credits": "210", "usd": "0.210" },
  "poll": "/v1/jobs/3f9c2c1e-6d7a-4b8f-a2e1-9c0b5d4e3f21",
  "message": "Running 30 items as a job. Poll the job for progress; each item is charged only if it passes."
}

approval_required

The purchase waits for an owner. reason is over_threshold (the amount is above the agent's per-purchase approval threshold) or over_budget (it would take the agent over its monthly budget). Two inputs at 7 credits for an agent whose threshold is 10 credits:

Response (example)
{
  "status": "approval_required",
  "approval_id": "b4c3d2e1-f0a9-4b8c-7d6e-5f4a3b2c1d0e",
  "reason": "over_threshold",
  "amount": { "credits": "14", "usd": "0.014" },
  "expires_at": "2026-09-30T18:04:11.512Z",
  "message": "This is above the agent's approval threshold. An owner has been asked to approve it; retry with approval_id once approved."
}

Response fields

The fields of a single-input answer. A batch answer has the same top-level fields except execution_id, result and reason, which move into items[].

Execute response fields
FieldMeaning
statuspassed, partial or failed for one input; completed for a batch.
execution_idThis item's id. A fallback that ran gives the id of the attempt that served the answer; the first attempt's id is in attempts[].
receipt_idThe receipt for the whole request: one per request, and one per job. Fetch it with GET /v1/receipts/{receipt_id}. See Receipts.
tool_id, task_typeThe tool that was run (its slug) and its task type. With a paused-tool substitution tool_id is the replacement and substituted_for names the tool you asked for.
check_versionThe version of the pass rule that judged the result (v1). It is on the receipt too.
chargedWhat was taken: the price on a pass, pro rata on a partial, 0 on a fail. On a batch, the total.
refundedWhat was held but not taken, already back in the balance. charged + refunded is the hold.
resultThe provider's answer, in the task type's output shape. Present on passed and partial only.
reasonWhy the item failed or was partial. See Reason codes.
served_by, attempts, fallback_noteOnly with fallback: true. See Fallback.
substituted_forThe paused tool you asked for, when fallback: true replaced it before the call.
pricing_modeper_call when the org is on per-call pricing for this tool; absent otherwise. See Per-call pricing.
replayed, notereplayed: true when this answer is the stored answer to an earlier request with the same idempotency key; note says so when its result data has been deleted. See Idempotency.
summaryBatches only: { items, passed, partial, failed }.
items[]Batches only, one per input in order: index, status, charged, result or reason, and the fallback fields.

Reason codes

reason is a short machine-readable string. A fail is never charged, except a failed check under per-call pricing. The only partial reason is results:found/n on web search, charged pro rata. Some reasons carry a value after a colon.

Reason codes on failed and partial items
reasonTask typesMeaning
no_resultallThe provider had no record, or the answer was empty: no email, no results, no company, no page.
catch_allfind_emailAn address was found but the domain accepts any address, so it can't be verified.
status:<value>find_email, verify_emailThe verifier's status wasn't definitive: status:unknown, status:catch_all, or for find_email status:invalid. Verify-email passes on valid and on invalid, because both are true answers.
domain_mismatch, name_mismatch, company_mismatchenrich_company, enrich_personThe record is about something else: its domain, name (similarity under 0.9) or company domain doesn't match your input.
field_missing:<field>enrich_company, enrich_personA required output field is empty: name, domain, employee_range or industry for a company; title or contact (no email, phone or LinkedIn URL) for a person.
results:<found>/<n>web_searchPartial: fewer valid, distinct URLs than the n you asked for. Charged price × found ÷ n, rounded up.
http:<status>, blocked_page, content_too_shortextract_urlThe page didn't answer 200 (http:403, http:none), looked like a captcha or block page, or had under 200 characters of main content.
provider_errorallThe provider failed on both attempts: a 5xx, a 429, another 4xx such as a rejected key, or a connection failure. Free: neither you nor Arettic pays.
timeoutallNo answer within 30 seconds, twice.
check_errorallArettic's checker threw on this result. Counted as a fail so you never pay for Arettic's bug.
interrupted, internal_error, cancelledallThe item was caught by a crash mid-call, an unexpected error, or a cancelled job before it ran. Released, never charged.
expiredjobsThe job hit its 2-hour limit before this item ran. Released.

Test keys

With a test key (sk_test_…) execute runs the mock provider for the tool's task type and applies the real pass rule with the same check_version. The answer has the same shape as live, plus test_mode: true, a charged of 0 and would_have_charged: the tool's listed price per success (0 on a fail, pro rata on a partial). No credits move, nothing is held, and no receipt is written, so there is no execution_id, receipt_id or refunded. Batches of any size up to 1,000 run inline, so a test key never answers queued, and it never needs an approval. A test key ignores max_price, idempotency_key, approval_id and fallback. The inputs that make each mock tool pass, fail or go partial are on Test mode.

Response (example): test key, one input
{
  "test_mode": true,
  "tool_id": "mock-find-email",
  "task_type": "find_email",
  "check_version": "v1",
  "charged": { "credits": "0", "usd": "0.000" },
  "status": "passed",
  "result": { "email": "[email protected]", "verification_status": "valid" },
  "would_have_charged": { "credits": "38", "usd": "0.038" }
}

A test batch answers status: "completed" with test_mode, tool_id, charged (0), would_have_charged (the sum), summary and items[]. Each item has index, status, result or reason, and its own would_have_charged instead of charged.

Batches

inputs takes 1 to 1,000 objects for one tool. All of them are validated and priced together: one bad input refuses the whole request with invalid_input, before anything is held, and the message lists the bad items by index. max_price caps the whole batch.

Poll GET /v1/jobs/{job_id} (the MCP tool is get_job; the SDKs have waitForJob and wait_for_job). While it runs the answer has status (queued or running), items (the count) and progress. When it is done (or expired) it also has receipt_id, charged, refunded, summary and items[] with every item's outcome and result, like a completed batch. The org also gets a job.completed webhook. Everything about jobs is on Batches and jobs.

curl: poll a job
curl https://api.arettic.com/v1/jobs/3f9c2c1e-6d7a-4b8f-a2e1-9c0b5d4e3f21 \
  -H "Authorization: Bearer $ARETTIC_API_KEY"

max_price

max_price is a cap on the whole request, in whole credits. When you leave it out, the cap is the request's own price. The price per item is read once, when the request is validated, and fixed for that request: price × items is compared with max_price before anything is held, and a request over the cap is refused with price_above_max (HTTP 402). There is no separate price-changed error. If a tool's price rises between two calls, the next call that is over your cap is refused the same way, and charged on every answer tells you what was taken.

Response (example): 2 inputs at 7 credits with max_price 13
{
  "error": {
    "code": "price_above_max",
    "message": "This costs up to 14 credits (7 × 2), above your max_price of 13.",
    "doc_url": "https://arettic.com/docs/errors#price_above_max",
    "retryable": false
  }
}

With fallback: true, the cap also limits the second tool: each item may fall back to a tool priced at or under max_price ÷ items. Without max_price that is the original tool's price, so a fallback never costs more than what you asked for. You can never be charged more than max_price, and never more than price × items.

Idempotency

Send an idempotency_key (1 to 200 characters) with every live purchase. It is scoped to the agent: the same agent sending the same key and the same request gets the first answer back, with replayed: true, and is never charged twice. "The same request" means the same tool and the same inputs; key order inside an input doesn't matter. The replay carries the stored result while it exists (7 days); after that it carries the receipt and the charges with a note. A replay never needs credits and never calls a provider.

Both SDKs add a fresh UUID to every execute call and reuse it on that call's retries, so a retry after a timeout replays instead of buying twice. Pass your own key (idempotencyKey in TypeScript, idempotency_key in Python) to keep retries safe across restarts of your program. Jobs work the same way: the same key while the job runs answers request_in_progress, and once the job has its receipt the replay returns its items like a completed batch. Test keys ignore the field.

Response (example): the same key sent again
{
  "status": "passed",
  "execution_id": "c02576cf-b3ce-4b0f-a30c-6e8aad4328ce",
  "receipt_id": "67a1fb46-c554-4cf6-b4e2-df1dbdf0e936",
  "tool_id": "hunter-verify-email",
  "task_type": "verify_email",
  "check_version": "v1",
  "charged": { "credits": "7", "usd": "0.007" },
  "refunded": { "credits": "0", "usd": "0.000" },
  "result": { "email": "[email protected]", "status": "valid" },
  "replayed": true
}
Response (example): the same key while the first request is still running
{
  "error": {
    "code": "request_in_progress",
    "message": "A request with this idempotency_key is still running. Retry in a few seconds to get its receipt.",
    "doc_url": "https://arettic.com/docs/errors#request_in_progress",
    "retryable": true
  }
}

Fallback

Fallback is opt-in with fallback: true. When an item fails its check, or the provider errors or times out, Arettic tries once more with the next-ranked tool for the same task type: a purchasable, active tool with a working provider connection, other than the one you asked for, with the best score in the item's region (a regional score when the input's domain points to a region the tool covers, else GLOBAL), then the lowest price, and priced at or under max_price ÷ items. The first attempt was already released, so only a passing tool is charged. There is at most one fallback per item, and one receipt lists both attempts.

The second attempt is opened under the same checks as a request (the agent's budget, the org's balance, quotas). If any of them says no, the item stays failed and fallback_note says why. Requests sent with an approval_id never fall back: an approval covers exactly the tool it was granted for. Jobs fall back per item too.

Fallback fields on an item
FieldMeaning
served_byThe tool that gave the answer, when it was the fallback. tool_id stays the tool you asked for.
attempts[]Both attempts in order: execution_id, tool_id, status, reason (if any) and charged. Present whenever a fallback was considered.
fallback_noteWhy no second attempt ran: no other tool fits within max_price, or the fallback wasn't allowed (budget, balance or quota).
charged, refundedSummed over both attempts: the failed attempt's full price is in refunded, the passing attempt's price in charged.
curl
curl https://api.arettic.com/v1/execute \
  -H "Authorization: Bearer $ARETTIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tool_id": "hunter-verify-email",
    "input": { "email": "[email protected]" },
    "fallback": true,
    "idempotency_key": "verify-jason-2"
  }'

Hunter can't verify the address (status:unknown) and ZeroBounce, at 6 credits, can. The 7 credits held for the first attempt go back; 6 are charged:

Response (example)
{
  "status": "passed",
  "execution_id": "d7e6f5a4-b3c2-4d1e-9f0a-8b7c6d5e4f30",
  "receipt_id": "e8f7a6b5-c4d3-4e2f-8a1b-9c0d1e2f3a41",
  "tool_id": "hunter-verify-email",
  "task_type": "verify_email",
  "check_version": "v1",
  "charged": { "credits": "6", "usd": "0.006" },
  "refunded": { "credits": "7", "usd": "0.007" },
  "result": { "email": "[email protected]", "status": "valid" },
  "served_by": "zerobounce-verify-email",
  "attempts": [
    {
      "execution_id": "2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d",
      "tool_id": "hunter-verify-email",
      "status": "failed",
      "reason": "status:unknown",
      "charged": { "credits": "0", "usd": "0.000" }
    },
    {
      "execution_id": "d7e6f5a4-b3c2-4d1e-9f0a-8b7c6d5e4f30",
      "tool_id": "zerobounce-verify-email",
      "status": "passed",
      "charged": { "credits": "6", "usd": "0.006" }
    }
  ]
}

A paused tool (an outage, a loss-making price or a provider problem) refuses requests with tool_paused. With fallback: true it is replaced before the call instead: the best other tool within the cap runs, tool_id is that tool, and substituted_for is the one you asked for. If no other tool fits, the answer is tool_paused.

Approvals

Every agent has a monthly budget (default $50) and an approval threshold (default: any single purchase over $20; owners can set it to never ask). A live request over the threshold, or one that would take the agent over its budget, answers HTTP 202 approval_required with an approval_id, and every owner gets an email with a one-click decision page. The same request sent again while the decision is pending answers the same approval_id and sends no second email. Approvals expire after 24 hours.

Poll GET /v1/approvals/{approval_id} (get_approval over MCP, waitForApproval in the TypeScript SDK) until status is no longer pending. When it is approved, send exactly the same request again with approval_id added. It then skips the threshold and budget checks, and the approval is used up. An approval is locked to its agent, tool, inputs and amount: change any of them and the answer is approval_mismatch. A rejected approval answers approval_rejected; an expired one answers approval_expired, so send a fresh request to ask again. The owner's side, the events and the dashboard are on Budgets and approvals.

curl: resend with the approval
curl https://api.arettic.com/v1/execute \
  -H "Authorization: Bearer $ARETTIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tool_id": "hunter-verify-email",
    "inputs": [{ "email": "[email protected]" }, { "email": "[email protected]" }],
    "approval_id": "b4c3d2e1-f0a9-4b8c-7d6e-5f4a3b2c1d0e"
  }'

Per-call pricing

Pay-per-success assumes honest inputs. If an org's fail rate on a tool is more than twice the tool's baseline over at least 200 calls, that org is moved to per-call pricing for that tool: every checked call is charged, pass or fail, at the provider's cost times the plan's multiplier, rounded up. Provider errors and timeouts stay free. The owners get an email saying why. The answer then carries pricing_mode: "per_call", and a failed item that was charged does include its result. The rule is reviewed after 30 days or 200 more calls and lifted when the fail rate is back in line. Checking inputs before you send them (real names, domains that exist) keeps you off it.

Errors

Anything the API refuses comes back as { "error": { "code", "message", "doc_url", "retryable" } }. doc_url links to the code's entry on Error codes, and retryable says whether sending the same request again later can work. Refusals about money, limits and approvals add "status": "declined" next to error. Nothing is held on any refusal. The SDKs raise every refusal as AretticApiError with the same fields.

Response (example): declined
{
  "status": "declined",
  "error": {
    "code": "insufficient_credits",
    "message": "This needs 1050 credits ($1.050); the org has 1000 ($1.000). Top up to continue.",
    "doc_url": "https://arettic.com/docs/errors#insufficient_credits",
    "retryable": false
  }
}
Error codes execute can answer, in the order they are checked
CodeHTTPWhen
unauthenticated401The key is missing, wrong or revoked, or the agent is disabled.
ip_not_allowed403The key has an IP allowlist and this request came from elsewhere.
rate_limited429Too many requests from this agent. The RateLimit-* and Retry-After headers say when to retry. See Rate limits.
payload_too_large413The body is over 1 MB.
invalid_input400A field is missing or malformed: no tool_id, both input and inputs, an empty batch, over 1,000 inputs, a bad max_price, idempotency_key, approval_id or fallback, or an input that fails its task type's check. The message names every problem. Nothing was charged.
unknown_tool404No tool with that id or slug for your key. Live keys can't see mock- tools.
tool_paused409The tool is paused and the request didn't ask for fallback, or no other tool fits.
not_purchasable409The tool is listed for information only, isn't live, or has no price yet.
price_above_max402price × items is above max_price.
idempotency_conflict409The idempotency_key was already used by this agent for a different request.
request_in_progress409The request with this idempotency_key is still running. Retryable.
insufficient_credits402The org's balance can't cover price × items. Declined.
trial_limit429The org has never topped up and this would take it over 50 trial credits held in the last hour. Declined, retryable.
approval_mismatch403 or 409The approval_id belongs to another agent (403), was already used, or the tool, inputs or amount differ from what was approved (409). Declined.
approval_rejected403An owner declined this purchase. Declined.
approval_expired410The approval is older than 24 hours. Send the request again to ask afresh. Declined.
credits_frozen403Arettic has frozen the org's credits. Reads still work. Declined.
provider_quota429The org reached today's call limit for this tool's provider. It resets at 00:00 UTC; other tools for the task still work. Declined, retryable.
aup_limit429More than 500 people lookups (find, enrich or verify) at one company domain today. Bulk collection of a company's staff isn't allowed. Declined.
internal_error500Something went wrong on Arettic's side. Nothing was charged. Retryable.

provider_error and check_failed on the error codes page are not refusals: on this endpoint they are reasons on a failed item (HTTP 200), never charged. approval_required is a status (HTTP 202), not an error.

Where next

Updated 2026-09-29 · This page as Markdown · JSON