Skip to main content

Using the API directly

The same workflows can be done via the REST API. The API itself is under /core/v1/. All endpoints that require authentication expect the API key in a header:
  • X-Api-Key: your-api-key
  • or Authorization: Bearer your-api-key
Optionally send X-Privacy-Mode: true on any request. The server then tags the request ID and suppresses its own log events for that request, so request details (including js_params and action output) are not written to Chipotle’s server logs. Use it whenever an action call carries anything you would not want in an operator’s logs. The Lit Agent Keychain sets it on every call. Examples below assume KEY=your_account_or_usage_api_key and BASE=https://api.chipotle.litprotocol.com (the hosted production API). The JavaScript examples use the Core SDK (LitNodeSimpleApiClient) from core_sdk.js, which lives at lit-static/core_sdk.js in the Chipotle repository. Two kinds of keys appear on this page. The account key is the key that new_account returns in step 1. It manages the account, so steps 3 to 6 send it. A usage key is a key you create with the account key in step 3. It runs actions in step 7. The account key can’t run actions, so /lit_action always needs a usage key. See Usage API Keys for the full rules. API workflow:
  1. New account or verify account (login)
  2. Add funds
  3. Add usage API key
  4. Create wallet (PKP)
  5. Add group and register IPFS action
  6. Add PKP to group (optional)
  7. Run lit-action

1. New account or verify account (login)

Create a new account (returns API key and wallet address). Or verify an existing key with the account_exists function.
The api_key in the response is your account key. Send both account_name and account_description, because the server requires them. The email field is optional.
The very first call takes ~15 seconds — account creation waits for an on-chain transaction on Base. Every other call below returns in well under a second.

2. Add funds

Lit Action execution and write/metered management calls (including the steps below) consume credits; read-only calls are free. Add funds with a credit card in the Dashboard, with crypto (ETH, USDC, SOL and more) or LITKEY, or programmatically via the billing API. Without credits the steps below return 402 Payment Required — see Errors. You can check your balance at any point:
balance_cents follows Stripe’s convention, so a negative number is credit you can spend. In this example, -500 means $5.00 of credit. Zero means your credits are used up. Paying by card through the API needs Stripe.js running in a browser, so the Dashboard is the simplest way to add your first funds.

3. Add usage API key

Create a usage API key with fine-grained permissions. The response includes the new key only once — store it immediately. Send the account key in the header for this call.
For a first test, use "execute_in_groups": [0]. Group 0 is a wildcard. A key with it can run any code that doesn’t use a PKP, without any group, so you can go straight to step 7 and come back to steps 4 to 6 when you need a wallet. The examples below grant group 1 instead. That is the group you create in step 5, so either create the group first or change the key later with update_usage_api_key. A wildcard key still needs a matching group to use a PKP. See Wildcard execution and wallet access.
Permission fields:

4. Create a wallet (PKP)

Request a new wallet (PKP) for the account. The server returns the wallet address and registers it. (Every account also starts with wallet id 0 — the Account Master Wallet created at signup.)
The wallet_address is also the PKP’s ID. Use it as pkp_id when you add the PKP to a group in step 6, and as pkpId inside your action.
GET /create_wallet also works but is deprecated — minting is a metered write, and GETs get replayed by prefetchers and retrying proxies. Use POST.

5. Add group and register IPFS action

Create a group, then add an action (IPFS CID) to scope which keys can run it. A group lists actions by CID, and the CID is a hash of the action’s code. Register the CID of the exact code you will send in step 7. If the code changes by even one character, its CID changes and step 7 returns 403. Compute the CID from the code first:
Then create the group and add the CID to it:
add_group returns group_id as a string, such as "1". Most requests on this page take it as a number, such as 1.

6. Add PKP to group (optional)

Restrict which wallets (PKPs) can be used in the group by adding their IDs to the group. A PKP’s ID is the wallet_address that create_wallet returned in step 4.

7. Run lit-action

Execute a lit-action by sending JavaScript code and optional params. Use a usage API key with execute permission in the header. The account key can’t run actions, so a call made with it returns 403. The server hashes your code to get its CID, checks that the key may run that CID, and then runs it in the TEE. Your main function receives js_params as its argument, and whatever main returns comes back as response.
The response has three fields, response, logs, and has_error. There is no separate field for signatures. If your action signs something, return the signature from main, as the JavaScript example does. If the key isn’t allowed to run this code, the call returns 403 with the message “The provided API key is not authorized to execute the specified action”. The usual causes are a key without execute permission in a group that lists this CID, or code that no longer matches the CID you registered. See Errors. You can send ipfs_id instead of code to run code the server has already received. The server keeps recently run code in a cache and does not fetch code from IPFS. If the CID isn’t in the cache, the call returns 400, so send code unless you know the server has it.
Newly-granted permissions are eventually consistent — verify before you depend on them. After add_usage_api_key (or add_action_to_group / add_pkp_to_group) there’s a short, variable delay before the grant works on /lit_action, so the first call right after minting a key can fail. Don’t sleep a fixed amount (it flakes or over-waits) — poll the real path (run the actual action with the actual key) until it succeeds, then proceed. A small retry on network errors and 5xx helps too. Worked version: waitForUsageKeyReady + retry wrapper in examples/action-bound-wallet.

Other useful endpoints

Raw CID vs hashed CID: Some endpoints accept a raw IPFS CID (action_ipfs_cid, e.g. "QmYour...") — the server hashes it for you. Other endpoints require the already-hashed CID (hashed_cid, e.g. "0xabc...") — a keccak256 hex string you get back from list_actions. As a rule: creation endpoints (add_action, add_action_to_group) take the raw CID; update/delete endpoints (delete_action, remove_action_from_group, update_action_metadata) take the hashed CID.
Read / list (no billing charge):
  • GET /list_api_keys?page_number&page_size — List usage API keys (paginated). Returns metadata only — key values are not returned.
  • GET /list_groups?page_number&page_size — List groups.
  • GET /list_wallets?page_number&page_size — List wallets (PKPs).
  • GET /list_wallets_in_group?group_id&page_number&page_size — List wallets in a group (group_id is a u64).
  • GET /list_actions?[group_id]&page_number&page_size — List actions. When group_id is provided, lists actions in that group. When omitted, lists all actions on the account.
  • GET /get_node_chain_config — Returns chain config, including contract addresses. No auth required.
  • GET /get_api_payers — Returns the list of API payer addresses. No auth required.
  • GET /get_admin_api_payer — Returns the admin payer address. No auth required.
  • POST /get_lit_action_ipfs_id — Compute the IPFS CID for a given JS code string. Body is a JSON string. No auth required. This is the same hash the server derives from code on every /lit_action call, so there is no separate upload or publish step: compute the CID, add it to a group, and send the source. For computing it locally (recommended in production) see Derived Actions.
Mutating management (billed):
  • POST /remove_usage_api_key — Delete a usage key. Body: {"usage_api_key": "..."}.
  • POST /update_usage_api_key — Update all permissions on an existing usage key. Body: same shape as add_usage_api_key, plus usage_api_key.
  • POST /update_usage_api_key_metadata — Update only the name/description of a usage key. Body: {"usage_api_key": "...", "name": "...", "description": "..."}.
  • POST /remove_group — Delete a group. Body: {"group_id": "..."}.
  • POST /update_group — Update group name, description, and permitted PKP IDs / CID hashes. Body: {"group_id": 1, "name": "...", "description": "...", "pkp_ids_permitted": [], "cid_hashes_permitted": []}.
  • POST /add_action — Register a standalone action (name + description + IPFS CID). Body: {"action_ipfs_cid": "Qm...", "name": "...", "description": "..."}.
  • POST /delete_action — Delete an action and its metadata from the account. Body: {"hashed_cid": "0x..."} (already-hashed CID).
  • POST /remove_action_from_group — Remove an IPFS action from a group. Body: {"group_id": 1, "hashed_cid": "0x..."} (already-hashed CID).
  • POST /update_action_metadata — Update the name/description of a registered action. Body: {"hashed_cid": "0x...", "name": "...", "description": "..."}.
  • POST /remove_pkp_from_group — Remove a PKP from a group. Body: {"group_id": 1, "pkp_id": "..."}.

Billing

  • GET /billing/stripe_config — Returns the Stripe publishable key. No auth required.
  • GET /billing/balance — Returns the current credit balance for the authenticated account.
  • POST /billing/create_payment_intent — Creates a Stripe PaymentIntent. Body: {"amount_cents": 500} (minimum 500 = $5.00). Returns client_secret for use with Stripe.js.
  • POST /billing/confirm_payment — Verifies a succeeded PaymentIntent and credits the account. Body: {"payment_intent_id": "pi_..."}.

ChainSecured-mode HTTP endpoints

The rest of this page is for ChainSecured accounts, where a wallet you control owns the account. If you haven’t chosen a mode yet, read API mode vs ChainSecured mode first. Four HTTP endpoints back the ChainSecured (sovereign) flow, and three of them take a wallet signature. They sit alongside the on-chain writes — see API mode vs ChainSecured mode for the full SDK-side workflow and when to choose each mode. The three signed endpoints share the same EIP-712 typed-data envelope. So does the X-Wallet-Auth header, which ChainSecured accounts send on billing calls because they have no account key. The wallet UI displays a labelled struct — address and issuedAt fields under a stable (name: "Lit ChainSecured", version: "1", chainId) domain — so users can see exactly what they’re signing. The primaryType pins the signature to a specific flow and is part of the EIP-712 type hash, so a signature minted for one endpoint is rejected by every other endpoint at the digest level. Typed-data payloads longer than 4 KiB (serialised JSON) are rejected. The canonical typed-data shape (must match exactly — field declaration order is part of the EIP-712 type hash):
The server enforces a ±5-minute window on issuedAt as the only replay protection — no nonce store. Worst-case replay on the unauthenticated mint endpoints just produces an extra unattached PKP (compute cost only — see each endpoint below for specifics). EOA and smart-contract wallets use identical parameters. Every flow above (and the X-Wallet-Auth billing header) accepts both, with no extra field or flag to set — you always post the same { typed_data, signature } (the message.address is your wallet address either way). The server first tries standard 65-byte ECDSA recovery; if the signature doesn’t recover to the claimed address, it treats that address as a contract and verifies on-chain via ERC-1271 isValidSignature(digest, signature) on the domain’s chain. Two value-level differences for the contract-wallet (e.g. Gnosis Safe, ZeroDev/Kernel) path:
  • signature is whatever the wallet produces — not necessarily a 65-byte ECDSA tuple. It may be longer (a Safe is roughly 65 bytes per owner plus overhead); the hard cap is 4 KiB.
  • The contract wallet must already be deployed on the domain’s chain. Counterfactual (ERC-6492) wallets are not supported.
The "0x<65-byte hex>" signatures shown in the request examples below are the EOA case; substitute your contract wallet’s signature bytes for the ERC-1271 path.

POST /create_wallet_with_signature

Derives a new PKP inside the TEE (from the root key and a fresh derivation path) after verifying a wallet-ownership signature. Used by createWallet in ChainSecured mode; the response is then passed to the on-chain registerWalletDerivation call (signed by the same wallet) to register the PKP to the account. No API key required. primaryType must be CreateWallet. Request body:
Response:
Pass derivation_path verbatim into registerWalletDerivation(adminHash, wallet_address, derivation_path, name, description) on the AccountConfig contract — until that lands, the PKP’s key can be derived but the PKP is not registered to any account. Here adminHash is the account’s apiKeyHash, its stable on-chain ID.
Prefer POST /prepare_wallet unless you specifically need the ownership signature. It returns the same { wallet_address, derivation_path } with no signature and no API key, and exists purely to streamline the ChainSecured wallet-creation ceremony:
  • One wallet prompt instead of two or three. The full signature flow is: sign the CreateWallet typed data → call this endpoint → sign (and wait on) the registerWalletDerivation transaction — plus an AddUsageApiKey signature if you’re attaching a usage key in the same flow. With prepare_wallet, the user signs exactly once: the on-chain registration transaction.
  • Faster. No round-trip to the user’s wallet before the mint, and no waiting between sequential prompts — you fetch the address, then go straight to the one transaction that matters.
  • Same security. The on-chain registerWalletDerivation (inside your owner-signed transaction) is the real authorization boundary; the CreateWallet signature authorizes nothing durable, so dropping it gives up nothing.

POST /prepare_wallet

The no-signature equivalent of create_wallet_with_signature. Returns a freshly derived { wallet_address, derivation_path } so you can register the PKP on-chain yourself — folding the whole ChainSecured owner ceremony into a single signed transaction (the bind UserOp) instead of one wallet prompt per mint. The wallet-ownership signature on create_wallet_with_signature authorizes nothing durable: the real authorization boundary is your own on-chain registerWalletDerivation, signed inside your owner transaction. So this endpoint safely omits it. No API key. No request body. The server generates the derivation path itself (a fixed 96-bit wallet-path prefix plus 160 bits of CSPRNG entropy); you cannot supply your own. registerWalletDerivation rejects a new path without that prefix, so always register the derivation_path returned by the server verbatim. Response:
Pass derivation_path verbatim into registerWalletDerivation(adminHash, wallet_address, derivation_path, name, description). Until that on-chain call lands, the PKP’s key can be derived but the PKP is registered to no account — an un-registered response is equivalent to a discarded keypair (compute-only cost, exactly like a replay of the signature endpoints).
prepare_wallet is not idempotent. Every call returns a brand-new wallet with a fresh random derivation path:
  • Retrying returns a different address. If a call succeeds but your network drops the response, calling again does not recover the same wallet — it mints another one. Treat each response as throwaway until you’ve registered it on-chain, and register exactly once.
  • Concurrent callers each get their own wallet. There is no server-side dedup. If two systems both “ensure the account has a wallet,” they will each get a different address and each fire a different bind UserOp, leaving the account with two wallets rather than one shared wallet. To converge on a single wallet, coordinate client-side (check the existing PKP registry via list_wallets first, or designate a single writer).
There is no meaningful collision risk between concurrent callers: each gets an independent 256-bit random path, so deriving the same address is cryptographically negligible. If the same address somehow were registered twice within one account, the second call reverts ("PKP already registered") — note that guard is per-account, so it does not prevent a different account from registering an address it observed elsewhere.

POST /convert_to_chain_secured_account

Flips a managed (API-mode) account to ChainSecured (unmanaged) in a single on-chain transaction. The account’s apiKeyHash is preserved — groups, PKPs, action metadata, and usage API keys (everything keyed by apiKeyHash on-chain) stay attached. Only the admin wallet and the managed flag change on-chain. Your credit balance carries over. Billing is tied to the account’s billing wallet, which is set when the account is created and stays the same through conversion, so charges keep going to the same Stripe customer. You can confirm the balance with GET /billing/balance after converting. There is no reverse path. Requires the existing master API key, which is the account key (sent via X-Api-Key or Authorization: Bearer, per the auth header conventions above). The server’s api_payer signs the on-chain conversion after verifying the wallet signature. An api_payer is a server wallet that submits on-chain writes for API-mode accounts, and GET /get_api_payers lists them. The new admin must sign EIP-712 typed data with primaryType: "ConvertAccount". Request body:
See API mode vs ChainSecured mode → Converting an API account to ChainSecured for the dashboard flow, the SDK wrapper (client.convertToChainSecuredAccount), and the post-conversion verification checklist.

POST /add_usage_api_key_with_signature

The ChainSecured counterpart to /add_usage_api_key. Mirrors create_wallet_with_signature: the server derives a usage-key wallet inside the TEE after verifying a wallet-ownership signature, then returns the secret (base64-encoded) plus the wallet address and derivation path. The client follows up on-chain — only the admin wallet of a ChainSecured account can call setUsageApiKey, so the server cannot complete the attach for you. No API key required. primaryType must be AddUsageApiKey. Worst-case replay just returns a fresh secret for an unattached wallet — equivalent to a freshly generated keypair until the admin wallet calls the on-chain follow-ups, so compute cost only. Request body:
Response:
The client must do two on-chain calls signed by the admin wallet to attach the new usage key:
  1. registerWalletDerivation(adminHash, wallet_address, derivation_path, name, description) — registers the PKP to the account.
  2. setUsageApiKey(adminHash, keccak256(usage_api_key_bytes), expiration, balance, name, description, …permissions) — attaches the usage key with its permission set.
Until both land, the secret in usage_api_key is just a freshly minted keypair with no on-chain identity.

Both request/response shapes and OpenAPI spec are available directly in the dev system. For a Swagger UI implementation of the OpenAPI spec, please browse to:

https://api.chipotle.litprotocol.com/core/v1/swagger-ui

Open API Specification

The OpenAPI spec itself can be found at:

https://api.chipotle.litprotocol.com/core/v1/openapi.json
Note that these specs are subject to minor changes and will always be available with the dev server endpoints.