Direct Execution API
The Direct Execution API allows you to execute blockchain transactions directly without creating workflows. All endpoints require API key authentication and are subject to rate limiting and spending caps.
Authentication
All direct execution endpoints require an organization API key (kh_) passed in the Authorization header as a bearer token:
Authorization: Bearer kh_your_api_keySee Authentication for the full auth model and API Keys for details on creating and managing API keys.
Rate Limits
Direct execution requests are limited to 60 requests per minute per API key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset so you can pace requests; a 429 adds Retry-After with the seconds to wait. See API errors for the full header reference.
Spending Caps
Organizations can configure daily spending caps in wei. If the cap is exceeded, execution requests return a 422 status with error code SPENDING_CAP_EXCEEDED.
Idempotency
Send an Idempotency-Key header to safely retry a request without risking a double-execution. The key is any client-chosen string (for example an agent-side transaction id, ideally a UUID).
- Replay: a retry with the same key and the same request body returns the original response (same
executionId, same status) without executing again. - Conflict: reusing a key with a different request body returns
409with codeidempotency_conflictand theoriginalExecutionIdthe key first produced. Use a new key for a different request. - In progress: a duplicate that arrives while the first request is still running returns
409with codeidempotency_in_progress; retry shortly. - Scope: keys are scoped per organization, so the same key is shared across an org’s API keys.
- Window: stored responses are replayable for 24 hours. After that the key is free to reuse.
Requests without an Idempotency-Key behave normally. Read-only and dry-run (simulate: true) requests are not affected.
curl -X POST https://app.keeperhub.com/api/execute/transfer \
-H "Authorization: Bearer kh_..." \
-H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-H "Content-Type: application/json" \
-d '{ "chainId": "8453", "recipientAddress": "0x...", "amount": "0.1" }'Workflow webhooks (POST /api/workflows/{workflowId}/webhook) accept the same header, scoped per workflow.
Transfer Funds
POST /api/execute/transferTransfer native tokens (ETH, MATIC, etc.) or ERC-20 tokens directly.
Request Body
{
"network": "ethereum",
"recipientAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"amount": "0.1",
"tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"gasLimitMultiplier": "1.2"
}Parameters:
network(required): Blockchain network name (e.g.,ethereum,base,polygon)recipientAddress(required): Destination wallet addressamount(required): Amount in human-readable units (e.g., “0.1” for 0.1 ETH or tokens)tokenAddress(optional): ERC-20 token contract address. Omit for native token transfers.tokenConfig(optional): JSON string with token metadata for non-standard tokens:{"decimals":18,"symbol":"USDC"}gasLimitMultiplier(optional): Gas limit multiplier (e.g., “1.5” for 50% buffer)
Response
{
"executionId": "direct_123",
"status": "completed"
}The execution runs synchronously. Status will be completed or failed when the request returns.
Call Smart Contract
POST /api/execute/contract-callCall any smart contract function. Automatically detects read vs write operations.
Request Body
{
"contractAddress": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"network": "ethereum",
"functionName": "balanceOf",
"functionArgs": "[\"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb\"]",
"abi": "[{...}]",
"value": "0.1",
"gasLimitMultiplier": "1.2"
}Parameters:
contractAddress(required): Smart contract addressnetwork(required): Blockchain network namefunctionName(required): Name of the function to callfunctionArgs(optional): JSON array string of function arguments (e.g.,"[\"0x...\", \"1000\"]")abi(optional): Contract ABI as JSON string. Auto-fetched from block explorer if omitted.value(optional): Native value to send with the call, as a decimal string in ether units (e.g.0.1) (for payable functions)gasLimitMultiplier(optional): Gas limit multiplier
Response
Read Function (view/pure):
{
"result": "1500000000000000000"
}Read functions return immediately with the result value.
Write Function:
{
"executionId": "direct_123",
"status": "completed"
}Write functions execute synchronously and return execution status.
Check and Execute
POST /api/execute/check-and-executeRead a contract value, evaluate a condition, and conditionally execute a write operation.
Request Body
{
"contractAddress": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"network": "ethereum",
"functionName": "balanceOf",
"functionArgs": "[\"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb\"]",
"abi": "[{...}]",
"condition": {
"operator": "gt",
"value": "1000000000000000000"
},
"action": {
"contractAddress": "0x...",
"functionName": "transfer",
"functionArgs": "[\"0x...\", \"500000000000000000\"]",
"abi": "[{...}]",
"gasLimitMultiplier": "1.2"
}
}Condition Operators:
eq: Equal toneq: Not equal togt: Greater thanlt: Less thangte: Greater than or equal tolte: Less than or equal to
Response
Condition Not Met:
{
"executed": false,
"condition": {
"met": false,
"observedValue": "500000000000000000",
"targetValue": "1000000000000000000",
"operator": "gt"
}
}Condition Met and Action Executed:
{
"executed": true,
"executionId": "direct_123",
"status": "completed",
"condition": {
"met": true,
"observedValue": "1500000000000000000",
"targetValue": "1000000000000000000",
"operator": "gt"
}
}Dry-Run Simulation
All three execute endpoints (/api/execute/transfer, /api/execute/contract-call, /api/execute/check-and-execute) accept a simulate flag on the body. When set to boolean true, the endpoint validates inputs, resolves the org’s from-address, encodes the call, and runs provider.estimateGas + provider.call against the chain — without signing or broadcasting a transaction.
No row is inserted into the execution audit table, no funds are reserved against the spending cap, and no transaction hash is produced. Use it to pre-flight a transaction (catch reverts, allowance mismatches, balance shortfalls, ABI mistakes) before spending gas.
Request
Add "simulate": true to any of the standard request bodies:
{
"contractAddress": "0x...",
"network": "ethereum",
"functionName": "transfer",
"functionArgs": "[\"0x...\", \"1000000\"]",
"abi": "[{...}]",
"simulate": true
}simulate must be a strict boolean — true or false. Strings ("true"), numbers (1), and other non-boolean values are rejected with HTTP 400 to prevent silent fall-through to a real broadcast. There is no query-string form; the body field is the only way to request a dry run.
Response — successful simulate
{
"success": true,
"status": "simulated",
"from": "0x...orgWallet",
"to": "0x...target",
"value": "1000000000000000000",
"gasEstimate": "65000",
"simulatedReturnValue": true,
"wouldRevert": false
}from: the org’s wallet address used as the sender (see “Known limitation” below)value: native value in wei sent with the callgasEstimate: gas the network would charge, as a decimal string in weisimulatedReturnValue: the decoded return value of the call (e.g.truefor ERC-20transfer, the read value for view functions,nullfor native transfers to an EOA recipient)wouldRevert: alwaysfalseon this path
Response — would-revert
When the chain would have rejected the transaction, the endpoint returns HTTP 400 with the decoded reason:
{
"success": false,
"status": "simulated",
"from": "0x...orgWallet",
"to": "0x...target",
"value": "0",
"wouldRevert": true,
"revertReason": "Error(ERC20: transfer amount exceeds balance)",
"error": "Error(ERC20: transfer amount exceeds balance)"
}Revert decoding tries (in order): the contract’s own ABI custom errors, common OpenZeppelin / standard errors, then the standard Error(string) revert (which is surfaced as Error(<message>)). If none match, the raw RPC error message is surfaced.
Token-transfer specifics
For ERC-20 transfers, decimals is optional — when omitted, the simulator looks up the token’s decimals() on-chain. tokenConfig is resolved through the same helper the broadcast path uses, so customToken, supportedTokenId, and legacy tokenConfig shapes all work identically.
check-and-execute specifics
simulate: true still evaluates the condition (which is read-only) and only swaps the action’s write for a simulated call. The response wraps the simulate body in the existing { executed, conditionResult } envelope:
{
"success": true,
"status": "simulated",
"from": "0x...",
"to": "0x...",
"gasEstimate": "65000",
"simulatedReturnValue": true,
"wouldRevert": false,
"executed": true,
"conditionResult": { "met": true, "...": "..." }
}executed reflects whether the action would have successfully run, so a reverted simulate returns executed: false.
Known limitation
The from address used during simulation is the org’s wallet (getOrganizationWalletAddress). Organizations that route writes through a Safe will see a simulation that reflects the EOA sending the call, not the Safe. Most config-bug categories (bad ABI, bad args, insufficient balance, allowance mismatches) still surface; Safe-routed msg.sender semantics do not.
Get Execution Status
GET /api/execute/{executionId}/statusCheck the status of a direct execution.
Response
{
"executionId": "direct_123",
"status": "completed",
"type": "transfer",
"transactionHash": "0x...",
"transactionLink": "https://etherscan.io/tx/0x...",
"gasUsedWei": "21000000000000",
"result": {...},
"error": null,
"createdAt": "2024-01-01T00:00:00Z",
"completedAt": "2024-01-01T00:00:15Z"
}Status Values:
pending: Queued for executionrunning: Currently executingcompleted: Successfully completedfailed: Execution failed
When polling this endpoint, honour the X-Poll-Interval-Hint response header instead of polling on a fixed timer: it gives the recommended number of seconds to wait before the next poll. A value of 0 means the execution has reached a terminal state (completed or failed) and you can stop polling.
Error Responses
Direct execution endpoints return detailed error information:
{
"error": "Missing required field",
"field": "network",
"details": "network is required and must be a non-empty string"
}Common Error Codes:
401: Invalid or missing API key422: Wallet not configured (see Wallet Management)429: Rate limit exceeded400: Invalid request parameters