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.
| Field | Type | Meaning |
|---|---|---|
| tool_id | string, required | The 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. |
| input | object | One input. Its fields depend on the tool's task type; see the table below. |
| inputs | array of 1 to 1,000 objects | A 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_price | whole number of credits, as a JSON number or a string of digits | The 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_key | string, 1 to 200 characters | Makes a retry safe: the same agent sending the same key gets the first answer back and is never charged twice. See Idempotency. |
| approval_id | approval UUID | From an approval_required answer, once an owner has approved it. The request must be exactly the approved one. See Approvals. |
| fallback | boolean, default false | If 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.
| task_type | Input fields | Checked before the call |
|---|---|---|
| find_email | first_name: string; last_name: string; domain: company domain, e.g. acme.com | first_name and last_name up to 100 characters. domain must look like a domain and must exist in DNS. |
| verify_email | email: string | email must be an email address of up to 254 characters, and its domain must exist in DNS. |
| enrich_company | domain: company domain; name: company name (if no domain); country: optional, with name | domain must look like a domain, or send name (up to 200 characters) instead. A domain must exist in DNS. |
| enrich_person | first_name: string; last_name: string; company_domain: company domain | first_name and last_name up to 100 characters. company_domain must look like a domain and must exist in DNS. |
| web_search | query: string, up to 500 characters; n: 1–25, default 5 | query up to 500 characters. n, if sent, is a whole number from 1 to 25; the default is 5. |
| extract_url | url: http(s) URL | url 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.
- Validate (free). The tool must exist, be purchasable and not paused;
inputorinputsmust be present and within the limits; every input passes the syntax check, then the DNS and public-host checks. A problem answersinvalid_input,unknown_tool,not_purchasableortool_paused, and no provider is called. - 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 × itemsis the most the request can cost. If it is overmax_price, the answer isprice_above_max. - Replay. With an
idempotency_keythat this agent already used for the same request, the first answer comes back withreplayed: true. Nothing runs and nothing is charged. - Authorize. The org's balance must cover
price × items, or the answer isinsufficient_credits. An org that has never topped up can hold at most 50 trial credits an hour, or the answer istrial_limit. Then, unless the request carries anapproval_id, the amount is checked against the agent's approval threshold and its monthly budget: over either, the answer isapproval_required(HTTP 202) and an owner is emailed. The agent can't change any of these limits. - 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_requiredwithover_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. - 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.
- Check. The published pass rule for the task type runs on the answer and gives
pass,partial(web search only) orfailwith a reason. Forfind_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 ischeck_version(v1), on the answer and on the receipt. If the checker itself throws, the item fails withcheck_error: you never pay for Arettic's bug. - 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 reasoninterrupted. - Deliver. A passed or partial item comes back with its
result. A failed item comes back with itsreasononly; 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 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"
}'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;
}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.
{
"name": "execute",
"arguments": {
"tool_id": "hunter-verify-email",
"input": { "email": "[email protected]" },
"max_price": 10,
"idempotency_key": "verify-jason-1"
}
}{
"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.
| status | HTTP | When | What comes with it |
|---|---|---|---|
| passed | 200 | One input, and the result passed the check. | result, charged (the price), refunded (0), execution_id, receipt_id. |
| partial | 200 | One input for web_search, and fewer results than asked for. | result, reason (results:2/5), charged pro rata, refunded the rest. |
| failed | 200 | One 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.) |
| completed | 200 | inputs 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. |
| queued | 202 | inputs with more than 25 items, with a live key. | job_id, items, max_charge, poll, message. Poll GET /v1/jobs/{job_id}. |
| approval_required | 202 | The 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. |
| declined | 402, 403, 409, 410 or 429 | The 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
{
"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:
{
"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:
{
"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.
{
"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:
{
"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[].
| Field | Meaning |
|---|---|
| status | passed, partial or failed for one input; completed for a batch. |
| execution_id | This 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_id | The 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_type | The 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_version | The version of the pass rule that judged the result (v1). It is on the receipt too. |
| charged | What was taken: the price on a pass, pro rata on a partial, 0 on a fail. On a batch, the total. |
| refunded | What was held but not taken, already back in the balance. charged + refunded is the hold. |
| result | The provider's answer, in the task type's output shape. Present on passed and partial only. |
| reason | Why the item failed or was partial. See Reason codes. |
| served_by, attempts, fallback_note | Only with fallback: true. See Fallback. |
| substituted_for | The paused tool you asked for, when fallback: true replaced it before the call. |
| pricing_mode | per_call when the org is on per-call pricing for this tool; absent otherwise. See Per-call pricing. |
| replayed, note | replayed: 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. |
| summary | Batches 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 | Task types | Meaning |
|---|---|---|
| no_result | all | The provider had no record, or the answer was empty: no email, no results, no company, no page. |
| catch_all | find_email | An address was found but the domain accepts any address, so it can't be verified. |
| status:<value> | find_email, verify_email | The 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_mismatch | enrich_company, enrich_person | The 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_person | A 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_search | Partial: fewer valid, distinct URLs than the n you asked for. Charged price × found ÷ n, rounded up. |
| http:<status>, blocked_page, content_too_short | extract_url | The 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_error | all | The 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. |
| timeout | all | No answer within 30 seconds, twice. |
| check_error | all | Arettic's checker threw on this result. Counted as a fail so you never pay for Arettic's bug. |
| interrupted, internal_error, cancelled | all | The item was caught by a crash mid-call, an unexpected error, or a cancelled job before it ran. Released, never charged. |
| expired | jobs | The 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.
{
"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.
- 25 or fewer run in the request. Every item is held at once, then up to 4 items run at a time, each settling as soon as it is checked. The answer is
completedwithitems[]. With slow providers a full batch can take longer than a client's default timeout (60 seconds in both SDKs); raise it, or send the batch as a job. - More than 25 become a job. The hold for every item is placed before the answer, the inputs wait encrypted, and a worker runs them 8 at a time with a 2-hour limit. The answer is
queued(HTTP 202) with ajob_id. Items not run within 2 hours are released with the reasonexpired.
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 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.
{
"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.
- Same key, same request, finished: HTTP 200, the first answer, plus
replayed: true. - Same key, same request, still running (or two identical requests sent at once): HTTP 409
request_in_progress, which is retryable. Only one of them runs and is charged; send it again in a few seconds to get the receipt. - Same key, different request: HTTP 409
idempotency_conflict. Use a new key for a new request. - No key: every request is a new purchase. A retry after a dropped connection buys again.
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.
{
"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
}{
"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.
| Field | Meaning |
|---|---|
| served_by | The 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_note | Why no second attempt ran: no other tool fits within max_price, or the fallback wasn't allowed (budget, balance or quota). |
| charged, refunded | Summed over both attempts: the failed attempt's full price is in refunded, the passing attempt's price in charged. |
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:
{
"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 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.
{
"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
}
}| Code | HTTP | When |
|---|---|---|
| unauthenticated | 401 | The key is missing, wrong or revoked, or the agent is disabled. |
| ip_not_allowed | 403 | The key has an IP allowlist and this request came from elsewhere. |
| rate_limited | 429 | Too many requests from this agent. The RateLimit-* and Retry-After headers say when to retry. See Rate limits. |
| payload_too_large | 413 | The body is over 1 MB. |
| invalid_input | 400 | A 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_tool | 404 | No tool with that id or slug for your key. Live keys can't see mock- tools. |
| tool_paused | 409 | The tool is paused and the request didn't ask for fallback, or no other tool fits. |
| not_purchasable | 409 | The tool is listed for information only, isn't live, or has no price yet. |
| price_above_max | 402 | price × items is above max_price. |
| idempotency_conflict | 409 | The idempotency_key was already used by this agent for a different request. |
| request_in_progress | 409 | The request with this idempotency_key is still running. Retryable. |
| insufficient_credits | 402 | The org's balance can't cover price × items. Declined. |
| trial_limit | 429 | The org has never topped up and this would take it over 50 trial credits held in the last hour. Declined, retryable. |
| approval_mismatch | 403 or 409 | The approval_id belongs to another agent (403), was already used, or the tool, inputs or amount differ from what was approved (409). Declined. |
| approval_rejected | 403 | An owner declined this purchase. Declined. |
| approval_expired | 410 | The approval is older than 24 hours. Send the request again to ask afresh. Declined. |
| credits_frozen | 403 | Arettic has frozen the org's credits. Reads still work. Declined. |
| provider_quota | 429 | The 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_limit | 429 | More 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_error | 500 | Something 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
- Receipts: the receipt field by field, with every item's provider, hashes, check version and outcome.
- Disputes: a charged result looks wrong? Dispute it within 7 days; decided within 48 hours against the stored copy.
- Batches and jobs: states, polling, expiry and the
job.completedwebhook. - Budgets and approvals: the owner's side of
approval_required. - Pass rules: what passes, what fails and what is refunded, per task type.
- Test mode: every mock tool and the inputs that make it pass, fail or go partial.
- Error codes and the JSON Schemas for the request, the response and the receipt.