SafeClick API

Enforcement API

Send the transaction or intended action before your system acts. SafeClick returns the decision your execution path must enforce.

First call

Pre-seed status: this customer ingress is defined in code and App Hosting configuration. An authenticated request on the current serving build has not yet been verified. Contact SafeClick for provisioned access before treating it as live.

Base URL
https://safeclick.dev/api/rest

Create a Team API key in the console, keep it on your server, then submit the transaction immediately before signing, broadcast, or execution.

curl -X POST "https://safeclick.dev/api/rest/v1/scan/transaction" \
  -H "x-api-key: $SAFECLICK_API_KEY" \
  -H "Idempotency-Key: $SAFECLICK_REQUEST_ID" \
  -H "content-type: application/json" \
  -d '{
    "to": "0x52908400098527886E0F7030069857D2E4169EE7",
    "value": "1000",
    "network_key": "eip155:1"
  }'

POST /v1/scan/transaction returns HTTP 202 with a scan_id, usually within a few seconds. Its status is usually processing: the response is a receipt for your scan, not a decision.

HTTP/1.1 202 Accepted

{
  "scan_id": "6d5b6c44-37d3-4d31-b6e1-3902b5c81616",
  "status": "processing",
  "action": null,
  "validation_required": false,
  "risk_tier": "unknown",
  "reason_categories": [],
  "coverage_status": "unknown",
  "response_schema_version": "public-scan-v1"
}

Read the scan with GET /v1/scans/{id}, using the scan_id, until status is final or failed.

export SCAN_ID="6d5b6c44-37d3-4d31-b6e1-3902b5c81616"

curl "https://safeclick.dev/api/rest/v1/scans/$SCAN_ID" \
  -H "x-api-key: $SAFECLICK_API_KEY"
HTTP/1.1 200 OK

{
  "scan_id": "6d5b6c44-37d3-4d31-b6e1-3902b5c81616",
  "status": "final",
  "action": "ALLOW",
  "validation_required": false,
  "risk_tier": "low",
  "reason_categories": ["policy_rule"],
  "coverage_status": "complete",
  "transaction_commitment": "0xabababababababababababababababababababababababababababababababab",
  "execution_authorized": true,
  "response_schema_version": "public-scan-v1"
}

What your integration needs to do

  1. Create a Team API key and keep it in a server-side secret store.
  2. Build the exact transaction you intend to execute and submit it to POST /v1/scan/transaction with a new Idempotency-Key.
  3. Store the scan_id from the HTTP 202 response together with the Idempotency-Key and the exact transaction you submitted.
  4. Poll GET /v1/scans/{id} every 5 seconds until status is final or failed, for at most 960 seconds counted from just before you sent the POST. Give each read its own timeout, no longer than the time left before that deadline.
  5. Execute only when status is final and action is ALLOW or FINAL_ALLOW, execution_authorized is true, and transaction_commitment is 0x plus 64 lowercase hex characters and equals the commitment you stored for the exact transaction you are about to sign, before that 960-second deadline. For Solana, use shadow mode for now.
  6. Treat failed, BLOCK, FINAL_BLOCK, a scan still waiting after 960 seconds, a timeout, or any error as do not execute.
  7. If the transaction changes in any way, submit a new scan with a new Idempotency-Key.

Wait for the decision

Poll GET /v1/scans/{id} every 5 seconds until status is final or failed. While status is processing or pending_validation (a WARN or HOLD waiting for validation), keep waiting and do not execute. Reading a scan does not start new work or use your allowance.

The POST response can already be final or failed. Read status on every response, including the first one, and skip polling when there is nothing left to wait for.

The maximum wait is 960 seconds from when SafeClick accepted the scan. A scan without a final decision by then ends as failed. After that the result does not change. Count your own wait from just before you send the POST. If it reaches 960 seconds, stop waiting and do not execute.

const SCANS_URL = "https://safeclick.dev/api/rest/v1/scans/";
const headers = { "x-api-key": process.env.SAFECLICK_API_KEY };
const POLL_MS = 5 * 1000;
const READ_TIMEOUT_MS = 10 * 1000;

// The 960 seconds run from when SafeClick accepted the scan, so start
// the clock just before sending the POST, not when its reply arrives.
const deadline = sentAt + 960 * 1000; // sentAt = Date.now() before POST /v1/scan/transaction

let result = submitted; // the HTTP 202 body from POST /v1/scan/transaction

while (result.status === "processing" || result.status === "pending_validation") {
  if (deadline - Date.now() <= POLL_MS) break; // no time for another read: do not execute
  await new Promise((resolve) => setTimeout(resolve, POLL_MS));
  const left = deadline - Date.now(); // a timer can fire late, so check the time again
  if (left <= 0) break; // do not execute
  try {
    // Abort a read that hangs at the deadline.
    const signal = AbortSignal.timeout(Math.min(READ_TIMEOUT_MS, left));
    const response = await fetch(SCANS_URL + result.scan_id, { headers, signal });
    if (response.status === 429 || response.status >= 500) continue; // read again next tick
    if (!response.ok) break; // do not execute
    result = await response.json();
  } catch {
    // The read timed out or the network failed: read again next tick.
  }
}

Enforce the decision

// expectedCommitment belongs to the transaction you are about to sign.
const mayExecute =
  Date.now() < deadline &&
  result.status === "final" &&
  (result.action === "ALLOW" || result.action === "FINAL_ALLOW") &&
  result.execution_authorized === true &&
  /^0x[0-9a-f]{64}$/.test(result.transaction_commitment) &&
  result.transaction_commitment === expectedCommitment;

if (!mayExecute) {
  throw new Error("SafeClick did not allow execution");
}

// transaction is the payload stored with expectedCommitment.
await signOrExecute(transaction);
ResponseMeaningYour action
final · ALLOW / FINAL_ALLOWSafeClick allows this exact transaction when execution_authorized is true and transaction_commitment is 0x plus 64 lowercase hex characters matching the commitment stored for that transaction.Continue under your own signing policy for that commitment.
final · BLOCK / FINAL_BLOCKSafeClick blocks this action.Do not sign, broadcast, or execute.
processing / pending_validation (WARN, HOLD)No final decision exists yet.Do not execute. Keep polling by scan_id.
failed · action nullSafeClick could not complete the scan.Do not execute. See failed scans below.
timeout / 401 / 429 / 5xxSafeClick did not return a usable decision.Do not auto-execute.

HTTP 202 from the POST does not tell you whether the decision is pending or final. Read status, action, execution_authorized, and transaction_commitment. Execute only when execution_authorized is true, the commitment is 0x plus 64 lowercase hex characters, and it equals the commitment stored for the transaction you are about to sign.

Failed scans

failed means SafeClick could not complete the scan. It is an operational failure, not a risk decision: action is null and the optional failure_reason says why. Do not execute the transaction. Retrying with the same Idempotency-Key returns the same failed scan.

failure_reasonMeaningWhat to do
processing_timeoutSafeClick did not reach a final decision within 960 seconds of accepting the scan. This includes a WARN or HOLD that was not resolved in time.Do not execute. If you still want the transaction, submit it as a new scan with a new Idempotency-Key.
validation_expiredThe scan needed validation (a user confirmation or an approval from your organization), and it was not completed before the scan's deadline.Do not execute. If you still want the transaction, submit it as a new scan with a new Idempotency-Key.
processing_failedSafeClick could not process this transaction.Do not execute. If you still want the transaction, submit it as a new scan with a new Idempotency-Key. Contact SafeClick if it keeps happening.

Exact transaction bytes

A result applies only to the exact bytes you submitted. Any change to a serialized transaction (EVM, Solana, or Stellar), such as a new signature, blockhash, nonce, or any other byte, needs a new scan with a new Idempotency-Key. For a structured request (fields such as to, value, and data, or a UserOperation), the result applies only to those exact values. SafeClick does not sign, broadcast, or check whether your transaction is still valid on chain.

Solana

With current processing times, a Solana result usually arrives after a typical Solana recent blockhash has expired, so the scanned transaction can no longer be broadcast as it was. For now, use SafeClick for Solana in shadow mode: broadcast as you normally would, then compare SafeClick's decision afterwards. Do not hold a Solana transaction waiting for SafeClick before broadcast.

Retries and idempotency

Set SAFECLICK_REQUEST_ID to a new, unique Idempotency-Key for each transaction you scan, and keep it for retries of that same request.

Retrying the POST with the same Idempotency-Key and the same request body returns the original scan_id, whether the scan is processing, final, or failed. It does not start another scan or charge again. Using the same key with a different request body returns HTTP 409. A request without an Idempotency-Key always starts a new scan, so its retries are not deduplicated.

Supported networks

Transaction, wallet, and contract scans accept these EVM networks. Send the network_key shown, or the matching chain_id.

  • Ethereum eip155:1
  • Ethereum Sepolia eip155:11155111
  • Base eip155:8453
  • Base Sepolia eip155:84532
  • Arbitrum One eip155:42161
  • Arbitrum Sepolia eip155:421614
  • Polygon PoS eip155:137
  • Polygon Amoy eip155:80002
  • BNB Smart Chain eip155:56
  • BNB Smart Chain Testnet eip155:97
  • Avalanche C-Chain eip155:43114
  • Avalanche Fuji eip155:43113
  • HyperEVM eip155:999
  • HyperEVM Testnet eip155:998
  • Arc Testnet eip155:5042002

Any other EVM chain, including Optimism, returns HTTP 400 with error unsupported_network. No quota is used.

Enforcement endpoints

MethodPathPurposeScope
POST/v1/scan/transactionSubmit a transaction before execution; returns a scan_id to pollscan.execute
GET/v1/scans/{id}Poll a scan until it is final or failedscans.read

Wallet and contract scans

POST /v1/scan/wallet and POST /v1/scan/contract check one address. They use the same API key, Idempotency-Key, and response fields as transaction scans.

  • The request waits for SafeClick's decision and returns it in the same response, with HTTP 202. status is final or pending_validation, never processing, so there is nothing to poll. Read status and action from the body; the HTTP code does not tell you the decision.
  • transaction_commitment and execution_authorized on a wallet or contract result belong to this address check, not to a transaction. They never authorize signing or sending a transaction. Before you sign, scan the transaction itself with POST /v1/scan/transaction.
  • coverage_status is always unknown in the POST response. GET /v1/scans/{id} reads the same scan later, with coverage_status taken from the stored evidence.
  • A scan that returns a decision uses one unit of your Team's allowance. Retrying with the same Idempotency-Key and the same body returns the original response without running the scan or charging again.
  • Set your own client timeout. A timeout is not a decision: do not proceed, and retry with the same Idempotency-Key and body.
FieldRequiredDescription
addressyesThe wallet or contract address: 20 to 128 letters and digits, for example an EVM 0x address.
network_keynoThe chain as a CAIP-2 ID, for example eip155:1 for Ethereum mainnet. An EVM chain must be one of the supported networks; any other returns HTTP 400 unsupported_network. Without a chain, SafeClick infers the chain family from the address format (EVM when the format does not tell) and uses that family's mainnet, Ethereum for EVM.
chain_idnoAn EVM chain ID as a positive integer, instead of network_key. If you send both, they must name the same chain, or the request returns HTTP 400.
origin_urlnoThe page or dApp that asked for this address, when you know it.
metadatanoA JSON object with your own context for this scan.

Wallet scan request and its response:

curl -X POST "https://safeclick.dev/api/rest/v1/scan/wallet" \
  -H "x-api-key: $SAFECLICK_API_KEY" \
  -H "Idempotency-Key: $SAFECLICK_REQUEST_ID" \
  -H "content-type: application/json" \
  -d '{
    "address": "0x52908400098527886E0F7030069857D2E4169EE7",
    "network_key": "eip155:1"
  }'
HTTP/1.1 202 Accepted

{
  "scan_id": "0f9e4a52-8d1c-4b7e-9a35-2c6d8e1f4b70",
  "status": "final",
  "action": "ALLOW",
  "validation_required": false,
  "risk_tier": "low",
  "reason_categories": ["policy_rule"],
  "coverage_status": "unknown",
  "transaction_commitment": "0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
  "execution_authorized": true,
  "response_schema_version": "public-scan-v1"
}

Contract scan request and its response:

curl -X POST "https://safeclick.dev/api/rest/v1/scan/contract" \
  -H "x-api-key: $SAFECLICK_API_KEY" \
  -H "Idempotency-Key: $SAFECLICK_REQUEST_ID" \
  -H "content-type: application/json" \
  -d '{
    "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "network_key": "eip155:1"
  }'
HTTP/1.1 202 Accepted

{
  "scan_id": "b3a1c9d4-6e2f-4a8b-8c7d-5e9f0a1b2c3d",
  "status": "final",
  "action": "BLOCK",
  "validation_required": false,
  "risk_tier": "critical",
  "reason_categories": ["contract_risk"],
  "coverage_status": "unknown",
  "transaction_commitment": "0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef",
  "execution_authorized": false,
  "response_schema_version": "public-scan-v1"
}
ResponseMeaningYour action
final · ALLOWSafeClick's policy found no reason to stop at this address.Continue under your own policy.
final · BLOCKSafeClick's policy blocks this address.Do not send to, call, or approve this address.
pending_validation · WARN or HOLDSafeClick found risk that needs review. During the beta this result does not change to FINAL_ALLOW or FINAL_BLOCK later.Do not treat it as an allow. Stop, or send it to your own review.

These routes and the intelligence reads below reuse detector capabilities behind SafeClick. They share the Team API-key scope shown beside each route, but they are not separate products or the canonical transaction-enforcement entry point.

MethodPathPurposeScope
POST/v1/scan/walletCheck a wallet address; the decision is in the responsescan.execute
POST/v1/scan/contractCheck a contract address; the decision is in the responsescan.execute
GET/v1/intel/domain/{domain}Read stored domain intelligenceintel.read
GET/v1/intel/address/{address}Read stored address intelligenceintel.read

Wallet and contract scan errors

POST /v1/scan/wallet and POST /v1/scan/contract return their decision in the same response. When they cannot, the body is JSON with error and a message or reason:

HTTPerrorMeaning
400invalid_requestThe body does not match the fields above, for example a missing address or two chain hints that disagree. message says what is wrong.
400unsupported_networkThe chain is not one SafeClick scans, for example Optimism. See the supported networks. No allowance is used.
401unauthenticated, key_paused, key_revokedThe API key is missing, invalid, expired, paused, or revoked.
403insufficient_scopeThe API key does not have the scan.execute scope.
403access_not_grantedYour Team cannot run scans right now. reason is absent (no access grant yet), disabled, expired, or exhausted. Ask SafeClick to open or extend access.
409idempotency_conflictreason payload_mismatch or in_flight. See the two paragraphs below.
429rate_limitedToo many requests. Wait for the number of seconds in the Retry-After header.
503pipeline_unavailable, access_grant_unavailableSafeClick could not give you a usable result. See the paragraph below.
HTTP/1.1 403 Forbidden

{ "error": "access_not_granted", "reason": "exhausted" }

HTTP/1.1 409 Conflict

{ "error": "idempotency_conflict", "reason": "payload_mismatch" }

HTTP 409 returns error idempotency_conflict. reason payload_mismatch means the Idempotency-Key was already used with a different request payload; use a new key for a new or changed scan. reason in_flight means SafeClick has not settled the first request with this key yet; it may still be running or be held after a failure. Retry later with the same key and request body, and contact SafeClick if it persists.

HTTP 503: SafeClick could not confirm the scan, so there is nothing to act on: do not execute. Retry with the same Idempotency-Key and request body once the service is available; a new key starts a second scan. If the scan never started, its quota hold is released and the retry runs it. If the scan may already have a decision whose response was lost, or the hold could not be released, the quota stays reserved until SafeClick settles it, and until then the retry returns 409 with reason in_flight.

Webhooks

SafeClick can send a signed HTTPS POST to your endpoint when a scan reaches a decision. A webhook is a notification. It carries no transaction_commitment or execution_authorized. Before you execute a transaction, read GET /v1/scans/{id} and apply the execute check above.

EventSent whendata
scan.completedA wallet, contract, or transaction scan reaches a final ALLOW or BLOCK.status final, action ALLOW or BLOCK
decision.blockSent as a separate event together with scan.completed when the final action is BLOCK.status final, action BLOCK
validation.requiredA scan returns WARN or HOLD.status pending_validation, action WARN or HOLD
validation.final_allowValidation of a WARN or HOLD finished with FINAL_ALLOW. Not sent during the beta.status final, action FINAL_ALLOW
validation.final_blockValidation of a WARN or HOLD finished with FINAL_BLOCK. Not sent during the beta.status final, action FINAL_BLOCK
Body fieldDescription
idThe event id, starting with evt_. Treat it as an opaque string. It stays the same on every retry: use it to drop duplicates.
typeOne of the event types above.
organization_idYour Team ID.
created_atWhen SafeClick created the event (ISO 8601).
data.scan_idThe scan this event is about. Read it with GET /v1/scans/{id}.
data.statusfinal or pending_validation.
data.actionALLOW, BLOCK, WARN, HOLD, FINAL_ALLOW, or FINAL_BLOCK.
data.reason_categoryOne coarse category from the same list as reason_categories in a scan result, for example threat_intelligence.

A test event from POST /v1/webhooks/test has the same envelope and headers, but its data is { test: true, scan_id, action, note } with a placeholder scan_id, and no status or reason_category. Its id is evt_ followed by a UUID.

POST /your/webhook/path HTTP/1.1
content-type: application/json
x-safeclick-signature: sha256=<64 hex characters>
x-safeclick-timestamp: 1761660192
x-safeclick-event: decision.block
x-safeclick-delivery: evt_3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c

{
  "created_at": "2026-10-28T14:03:12.511Z",
  "data": {
    "action": "BLOCK",
    "reason_category": "threat_intelligence",
    "scan_id": "6d5b6c44-37d3-4d31-b6e1-3902b5c81616",
    "status": "final"
  },
  "id": "evt_3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c",
  "organization_id": "9b1f7c2e-4d5a-4e8b-a6c3-1f2e3d4c5b6a",
  "type": "decision.block"
}
HeaderValue
content-typeapplication/json
x-safeclick-signaturesha256= followed by the hex HMAC-SHA256 signature.
x-safeclick-timestampUnix time in seconds when this attempt was signed.
x-safeclick-eventThe event type, the same as type in the body.
x-safeclick-deliveryThe event id, the same as id in the body.

Verify every request before you trust it:

  1. Read the request body as the raw string you received, before parsing JSON. Re-serializing the parsed object changes the bytes.
  2. Compute HMAC-SHA256 over the timestamp header value, a period, and the raw body (`${timestamp}.${rawBody}`), with your endpoint's whsec_ signing secret as the key, and hex-encode it.
  3. Compare it with the value after sha256= in x-safeclick-signature, in constant time.
  4. Reject the request if the timestamp is more than 300 seconds away from your clock.
  5. Answer with any 2xx status within 10 seconds, then do your own processing. Drop events whose id you have already handled.
import { createHmac, timingSafeEqual } from "node:crypto";

function verifySafeClickWebhook(rawBody, headers, secret) {
  const timestamp = String(headers["x-safeclick-timestamp"] ?? "");
  const signature = String(headers["x-safeclick-signature"] ?? "");
  if (!/^\d+$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const presented = signature.startsWith("sha256=") ? signature.slice("sha256=".length) : "";
  if (!/^[0-9a-f]{64}$/.test(presented)) return false;
  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`, "utf8")
    .digest("hex");
  return timingSafeEqual(Buffer.from(presented, "hex"), Buffer.from(expected, "hex"));
}

Delivery and retries:

  • A delivery counts as received when your endpoint answers with any 2xx status within 10 seconds. SafeClick does not follow redirects.
  • A 5xx, a 429, a response timeout, or a connection error is retried. Any other status, including 3xx and other 4xx, fails the delivery at once, and it is not retried.
  • Before each round SafeClick resolves your hostname and checks that every address it returns is public. If that lookup fails or takes longer than 2 seconds, or an address is not public, nothing is sent and the delivery fails at once without a retry; last_error says why, for example webhook destination hostname could not be resolved.
  • Each round makes up to 3 attempts in quick succession. If they all fail, SafeClick tries again in later rounds, with the wait between rounds growing from about 8 seconds to about a minute; the retry queue is checked every 30 seconds. After 9 attempts in total the delivery is marked failed and is not retried.
  • A round is signed once: its attempts carry the same bytes and timestamp. Each later round is signed again, with a new timestamp and the endpoint's current secret, and keeps the same event id. scan.completed and decision.block are separate deliveries. Events can arrive out of order or more than once.
  • If SafeClick cannot read or decrypt your endpoint's signing secret, it never sends the event unsigned. It keeps the event and tries the secret again later with a growing wait. Each failed try uses one unit of the same budget as delivery attempts: a new round starts only while fewer than 8 have been used, so there are at most 8 failed tries, fewer if delivery attempts failed first. The delivery history shows last_error secret_lookup_failed, and when the budget is used up the delivery is marked failed.
  • If a delivery shows skipped_no_secret, SafeClick has no signing secret it can sign with for that endpoint, and the event is not sent. Rotate the secret and check that the response says delivery_enabled true.
  • After you rotate the secret, later rounds are signed with the new one, but a round already under way keeps its old signature. Accept both the old and the new secret on your receiver for a few minutes after rotating; a rejected signature fails that delivery for good.
  • A failed scan (status failed) sends no webhook. Read it with GET /v1/scans/{id}.

Set up and test:

  1. Register an endpoint with POST /v1/webhooks and a JSON body with url (a public https URL, at most 2048 characters) and optional events (a list of event types; leave it out or send an empty list for all events). A Team can have 10 active endpoints, and the same URL cannot be registered twice (HTTP 409).
  2. The response contains secret, the whsec_ signing secret. It is shown only once, here and when you rotate it with POST /v1/webhooks/{id}/rotate. Keep it on your receiver.
  3. Send a signed sample with POST /v1/webhooks/test and the body {"secret": "whsec_...", "deliver": true, "event_type": "decision.block"}. SafeClick delivers it to your endpoints that use this secret. It is not a real scan; its data differs from a real event, as described above.
  4. Read delivery attempts with GET /v1/webhooks/{id}/deliveries, or open Webhooks in the console.

Team configuration

MethodPathPurposeScope
GET/v1/access-grantRead this Team's API access allowance and expiryauthenticated
GET/v1/policiesList policy versionspolicy.read
POST/v1/policiesCreate a policy versionpolicy.write
GET/v1/webhooksList webhook endpointswebhooks.read
POST/v1/webhooksCreate a webhook endpointwebhooks.manage
DELETE/v1/webhooks/{id}Remove a webhook endpointwebhooks.manage
POST/v1/webhooks/{id}/rotateRotate a webhook secretwebhooks.manage
GET/v1/webhooks/{id}/deliveriesRead delivery attemptswebhooks.read
POST/v1/webhooks/testBuild or deliver a signed test eventwebhooks.manage
GET/v1/usageRead metered API usageusage.read
POST/v1/access-requestsRequest Team API accesssession-only

Decision audit trail

The Team console reads tenant-scoped audit records committed by the Enforcement pipeline. They show the decision path, actor, time, and customer-safe metadata. This is a decision audit trail, not a complete log of every administrative action.

Authentication

Send the Team API key as x-api-key or Authorization: Bearer. Keep it in a server-side secret store. Browser bundles and public repositories must not contain it.

New keys start with scan.execute and scans.read. During pre-seed, additional control-plane scopes are provisioned with SafeClick; request the scope shown beside the route before integrating it.

Keys are scoped to one Team. The API derives the Organization from the verified credential; request bodies cannot select another tenant.

Real scans require an active Team access grant from SafeClick. Check its status at GET /v1/access-grant. Configuration and existing scan reads remain available without a grant.

Errors

400The request does not match the input contract. error unsupported_network: the network is not one SafeClick scans; no quota is used.
401The API key is missing, invalid, expired, or revoked.
403The credential lacks scope, or access_not_granted explains the Team's missing, disabled, expired, or exhausted allowance.
404The Team cannot read the requested resource.
409reason payload_mismatch: the Idempotency-Key was already used with a different request body; use a new key for a new or changed transaction. reason in_flight: SafeClick has not settled the first request with this key yet; it may still be running or be held after a failure. Do not execute; retry later with the same key, and contact SafeClick if it persists.
429The caller or Team exceeded the current request budget. Honor Retry-After.
503No usable response, so do not execute. The scan may already exist, even with a result. Retry with the same Idempotency-Key and request body; a new key starts a second scan.

Get an API key

Pre-seed Team and API-key access is provisioned by SafeClick. Contact us to start an integration. Existing Team operators manage credentials under API keys. The plaintext key is shown once.