# Contractless PHP API HTTP API foundation for applications that communicate with Contractless nodes through `contractless/contractless-php-rpc`. ## Install ```bash composer install cp api.env.example api.env ``` Set one or more RPC nodes in `api.env`. The API has no wallet file, decryption key, public key, or private key of its own. The environment file must not be placed inside `public/`. Point the web server document root to the `public` directory. During local development, PHP's built-in server can be started from the project root: ```bash php -S 127.0.0.1:8080 -t public public/index.php ``` The included `public/.htaccess` routes Apache and normal Hestia Nginx-to-Apache installations through `public/index.php`. Apache must have `mod_rewrite` enabled and permit `.htaccess` overrides. For a pure Nginx or Nginx-to-PHP-FPM installation, add this inside the domain's server configuration: ```nginx location / { try_files $uri $uri/ /index.php?$query_string; } ``` After changing an Nginx configuration, validate and reload it: ```bash sudo nginx -t sudo systemctl reload nginx ``` The version root is available at `GET /api/v1`. ## Wallet Authentication Except for `GET /api/v1/live` and CORS `OPTIONS` requests, callers provide the wallet identity used for the node RPC handshake: ```text X-Contractless-Address: 40-character-address.cltc X-Contractless-Public-Key: Falcon-public-key-hex X-Contractless-Signature: 1332-character-signature-hex ``` `X-Contractless-Public-Key` may contain either the raw 897-byte Falcon public key or the 898-byte wallet representation with its leading network byte. `X-Contractless-Signature` is the caller's Falcon signature of the 32-byte Skein-256 digest of the exact ASCII text `aced`. The browser wallet or application creates this proof locally. The API never receives the private key and never signs on behalf of the user. Before opening an RPC connection, the API verifies: - The address, public key, and signature have the required sizes and formats - The public key satisfies the Contractless public-key rule - The wallet address is derived from the supplied public key - The supplied signature validates against the Contractless handshake digest The verified public key and signature are then passed to the PHP RPC library as a `HandshakeProof`. Every HTTP request creates its RPC client from that caller's proof. The API operator does not maintain a registered hot wallet. This handshake signature identifies the caller to a Contractless node. It does not authorize spending. Transactions must still be created and signed locally by the user's wallet and submitted as complete signed transaction bytes. Because the handshake proof can be reused by anyone who sees it, production APIs must use HTTPS. Replaying it can identify a caller for public lookups, but cannot produce valid wallet transactions without the user's private key. For example: ```bash curl https://api.example.com/api/v1/height \ -H "X-Contractless-Address: 0123456789abcdef0123456789abcdef01234567.cltc" \ -H "X-Contractless-Public-Key: $CONTRACTLESS_PUBLIC_KEY_HEX" \ -H "X-Contractless-Signature: $CONTRACTLESS_HANDSHAKE_SIGNATURE_HEX" ``` A browser wallet uses the same headers: ```javascript const response = await fetch(`${apiEndpoint}/api/v1/height`, { headers: { "X-Contractless-Address": wallet.address, "X-Contractless-Public-Key": wallet.publicKeyHex, "X-Contractless-Signature": wallet.handshakeSignatureHex, }, }); const result = await response.json(); ``` The wallet can generate the handshake proof when it is unlocked and reuse it for later API calls. It does not send its private key or wallet decryption key. ### Test Tool The CLI-only test tool loads a wallet locally, prompts privately for its decryption key, generates the handshake proof, and tests the default `/api/v1/height` route: ```bash php tools/test_api.php \ https://api.contractless.dev \ /private/path/to/test.wallet ``` An alternative GET route may be provided: ```bash php tools/test_api.php \ https://api.contractless.dev \ /private/path/to/test.wallet \ /api/v1/network ``` Wallet decryption and Falcon signing happen only inside the CLI process. The tool sends the address, public key, and handshake signature to the API. It never sends the wallet file, private key, or decryption key and returns a nonzero exit status when the HTTP request fails. The test tool requires the PHP GD and OpenSSL extensions in addition to the Contractless Skein and Falcon modules. If the HTTP API returns `503`, the tool also tests every RPC endpoint from the local `api.env` directly. It prints the real transport, handshake, or protocol error to the terminal without exposing that diagnostic through the public API. ## Initial Endpoints All responses use JSON. Successful responses contain `success: true` and a `data` object. Errors contain `success: false` and a sanitized `error` message. | Method | Route | Purpose | | --- | --- | --- | | `GET` | `/api/v1` | API version information | | `GET` | `/api/v1/live` | Confirm the PHP API configuration and process are reachable | | `GET` | `/api/v1/ready` | Confirm an RPC node is reachable and show endpoint health | | `GET` | `/api/v1/health` | Confirm RPC availability and return chain height | | `GET` | `/api/v1/network` | Return decoded network information | | `GET` | `/api/v1/height` | Return the current block height | | `GET` | `/api/v1/time` | Return the node's Unix timestamp | | `GET` | `/api/v1/addresses/validate?address=...` | Validate a registered canonical address | | `GET` | `/api/v1/addresses/vanity/resolve?address=...` | Resolve a vanity address to its owner | | `GET` | `/api/v1/addresses/registration?address=...` | Check wallet registration | | `POST` | `/api/v1/addresses/register` | Submit a locally signed wallet registration | | `GET` | `/api/v1/balances/base?coin=CLTC&address=...` | Return one base-coin balance | | `GET` | `/api/v1/balances?address=...` | Return every balance owned by an address | | `GET` | `/api/v1/transactions/lookup?txid=...` | Return a confirmed transaction | | `GET` | `/api/v1/addresses/history?address=...&skip=0&limit=100` | Return paginated history | | `POST` | `/api/v1/messages/verify` | Verify a signed message | | `POST` | `/api/v1/transactions/broadcast` | Broadcast a complete signed transaction | Message verification accepts: ```json { "message": "Exact message bytes", "address": "40-character-address.cltc", "signature": "1332-character-signature-hex" } ``` Transaction broadcasting accepts: ```json { "transaction_hex": "complete-signed-transaction-hex" } ``` The API never creates or signs a transaction for the caller. It only forwards the complete signed transaction through `Client::submitTransaction()`. Wallet registration accepts: ```json { "address": "40-character-address.cltc", "public_key": "1794-character-public-key-hex", "signature": "1332-character-registration-signature-hex" } ``` The authenticated address and public key must match the registration body. The API forwards the registration through `Client::registerWallet()`; it never creates the registration signature. Browser extensions must also be included in `API_CORS_ORIGINS` using their exact origin: ```text API_CORS_ORIGINS=https://wallet.example.com,chrome-extension://extension-id ``` ## Reliability Each RPC request still opens one connection, makes one request, receives one reply, and closes the connection. The API does not create persistent node connections. The endpoint pool retries another configured node only when a connection, timeout, read, or write failure occurs. A protocol error or rejected response is returned as a failure instead of being hidden by another node. Endpoint health survives separate PHP requests in the locked API state file: ```ini CONTRACTLESS_RPC_TIMEOUT=10 RPC_FAILURE_THRESHOLD=2 RPC_COOLDOWN_SECONDS=30 ``` After the configured number of consecutive transport failures, that endpoint is skipped for the cooldown period. A successful request clears its failure count. If every endpoint is cooling down or unreachable, the API returns `503 Service Unavailable`. Use `GET /api/v1/live` for a process liveness check. It does not require wallet headers or contact a Contractless node. Use `GET /api/v1/ready` with wallet headers when a deployment check must confirm that at least one RPC node can answer. Malformed upstream protocol replies return `502 Bad Gateway`. Internal API failures return a sanitized `500` response. Every response includes an `X-Request-ID` header. Server error logs contain that identifier and the error class, but never request bodies, wallet authentication headers, private keys, or raw node responses. Paid storage lookups remain pinned to the endpoint that generated their quote. They are never moved to another endpoint because payment addresses and lookup prices can differ between node operators. Rate limits, HMAC nonces, and endpoint health share the same file-backed state store. Every read-modify-write operation holds an exclusive file lock, so concurrent PHP workers cannot overwrite each other's updates. ## API Security General HTTP security controls run before caller wallet authentication or RPC client construction. This prevents rejected or rate-limited requests from triggering Falcon verification. ### HTTPS `API_REQUIRE_HTTPS=true` rejects non-HTTPS production requests. For direct TLS, no proxy configuration is needed. If HTTPS terminates at a reverse proxy, add only IP addresses or CIDR ranges belonging to that proxy: ```ini API_TRUSTED_PROXIES=127.0.0.1,10.20.0.0/16 ``` `X-Forwarded-For` and `X-Forwarded-Proto` are ignored unless the immediate connection came from a configured trusted proxy. Never add arbitrary public networks merely to make forwarded headers work. For local development without HTTPS: ```ini APP_ENV=development API_REQUIRE_HTTPS=false ``` ### CORS List the exact websites or browser extensions permitted to call the API: ```ini API_CORS_ORIGINS=https://wallet.example.com,chrome-extension://extension-id ``` Use `API_CORS_ORIGINS=*` only when intentionally operating a public API for every website. Requests without an `Origin` header, including normal server-to-server calls, are unaffected. ### Request Limits ```ini API_MAX_BODY_BYTES=2000000 API_RATE_WINDOW_SECONDS=60 API_PUBLIC_REQUESTS_PER_WINDOW=120 API_MESSAGE_VERIFY_REQUESTS_PER_WINDOW=30 API_BROADCAST_REQUESTS_PER_WINDOW=10 ``` Public callers are limited by resolved client IP. Message verification and transaction broadcasting have additional route-specific limits. Rate-limit responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and, when blocked, `Retry-After`. The default security state is stored in `storage/security-state.json`. Its directory must be writable by the PHP process and must remain outside the web document root. All `POST`, `PUT`, and `PATCH` requests require `Content-Type: application/json`. Oversized bodies are rejected before parsing. The API does not write request bodies to its application logs. ### Transaction Broadcasting Before contacting a node, the broadcast route decodes the first transaction byte and checks the exact serialized length expected by Contractless. Unsupported types, malformed lengths, genesis transactions, and node-generated reward transactions are rejected by the API. ### Optional API Keys API keys provide identified clients with their own quota: ```ini API_KEYS=example-service=replace-with-at-least-32-random-characters:300 ``` The client sends the ID and secret together: ```text X-API-Key: example-service.replace-with-at-least-32-random-characters ``` Multiple entries are comma-separated. API keys require HTTPS but do not provide request signing or replay protection. ### Optional HMAC Authentication HMAC authentication is intended for trusted server-to-server applications: ```ini API_HMAC_KEYS=backend=replace-with-at-least-32-random-characters:600 API_HMAC_CLOCK_SKEW_SECONDS=300 ``` The caller sends: ```text X-API-Key: backend X-Timestamp: current Unix timestamp X-Nonce: unique random value containing 16 to 128 letters, numbers, _ or - X-Signature: lowercase hexadecimal HMAC-SHA256 signature ``` The signed canonical string is: ```text HTTP_METHOD /request/path raw=query&string unix_timestamp nonce sha256_hex_of_exact_request_body ``` The signature is: ```php $bodyHash = hash('sha256', $exactRequestBody); $canonical = implode("\n", [ strtoupper($method), $path, $rawQueryString, (string) $timestamp, $nonce, $bodyHash, ]); $signature = hash_hmac('sha256', $canonical, $hmacSecret); ``` Timestamps outside the configured clock window are rejected. Each valid nonce is recorded until its timestamp can no longer be accepted, preventing replay. The browser wallet uses public rate-limited access. It must never contain an API-key secret or HMAC secret. ## Remaining RPC Endpoints ### Chain, Blocks, Headers, And Torrents | Method | Route | Purpose | | --- | --- | --- | | `GET` | `/api/v1/chain/difficulty` | Current height and next-block difficulty | | `GET` | `/api/v1/chain/largest-transaction-fee` | Largest eligible mempool fee | | `GET` | `/api/v1/chain/transaction-counts` | Confirmed totals by transaction type | | `GET` | `/api/v1/blocks/latest` | Latest raw block | | `GET` | `/api/v1/blocks/by-height?height=...` | Raw block at a height | | `GET` | `/api/v1/blocks/by-hash?hash=...` | Raw block matching a hash | | `GET` | `/api/v1/blocks/hash?height=...` | Block hash at a height | | `GET` | `/api/v1/headers/by-height?height=...` | Raw header at a height | | `GET` | `/api/v1/headers/by-hash?hash=...` | Raw header matching a hash | | `GET` | `/api/v1/headers/all` | Complete raw header history | | `GET` | `/api/v1/torrents/by-height?height=...` | Raw torrent metadata | Raw blocks, headers, and torrents are returned as `hex` plus their original byte count. The API does not alter the blockchain artifact. Large routes have a separate quota: ```ini API_EXPENSIVE_REQUESTS_PER_WINDOW=5 ``` Operators can disable costly raw routes independently: ```ini API_ENABLE_RAW_BLOCKS=true API_ENABLE_TORRENTS=true API_ENABLE_ALL_HEADERS=false ``` Complete header-history downloads are disabled by default. ### Mempool | Method | Route | Purpose | | --- | --- | --- | | `GET` | `/api/v1/mempool/count` | Number of pending transactions | | `GET` | `/api/v1/mempool/by-signature?signature=...` | Pending transaction by signature | | `GET` | `/api/v1/mempool/by-address?address=...` | Pending transactions involving an address | Pending transactions are returned as their exact signed transaction hex, original byte count, and transaction type. ### Tokens, NFTs, And RWAs | Method | Route | Purpose | | --- | --- | --- | | `GET` | `/api/v1/tokens` | Token list and origin transaction IDs | | `GET` | `/api/v1/tokens/catalog` | Token catalog | | `GET` | `/api/v1/tokens/details?name=...` | Details for one token | | `GET` | `/api/v1/nfts` | NFT/RWA list, ownership type, and supply | | `GET` | `/api/v1/nfts/details?name=...&series=...` | Details for one NFT/RWA item | | `GET` | `/api/v1/nfts/media?name=...&series=...` | NFT/RWA metadata and API image location | | `GET` | `/api/v1/nfts/image?name=...&series=...` | Cached NFT/RWA image or common placeholder | #### NFT And RWA Media NFT and RWA transactions store an IPFS CID rather than placing media directly on the blockchain. API operators choose whether their API retrieves and caches that media: ```ini LOCAL_CACHE=NO IPFS_GATEWAY=https://gateway.pinata.cloud/ipfs/ NFT_CACHE_PATH=storage/nft-cache NFT_PLACEHOLDER_IMAGE=public/images/nft-placeholder.svg NFT_IPFS_TIMEOUT_SECONDS=10 NFT_METADATA_MAX_BYTES=1048576 NFT_DOWNLOAD_MAX_BYTES=25000000 NFT_IMAGE_MAX_BYTES=5000000 NFT_IMAGE_MAX_WIDTH=1200 NFT_IMAGE_MAX_HEIGHT=1200 NFT_IMAGE_MAX_SOURCE_PIXELS=40000000 ``` The default IPFS gateway is the same gateway used by the Contractless GUI wallet. When `LOCAL_CACHE=NO`, the API does not contact the IPFS gateway. It returns the CID from the NFT transaction, `image_available: false`, and an image URL that serves the common placeholder. When `LOCAL_CACHE=YES`, the API: 1. Gets the CID from the NFT transaction returned by the configured node. 2. Checks `NFT_CACHE_PATH` using the CID and series number. 3. Retrieves missing metadata and media through `IPFS_GATEWAY`. 4. Returns the placeholder when the gateway times out or the content is unavailable or invalid. 5. Uses PHP GD to reduce images that exceed the configured dimensions or cached-image byte limit. 6. Stores the metadata and processed image locally. 7. Serves future requests from the local cache. For an NFT series, the API follows the GUI wallet convention and requests `CID/metadata/SERIES.json`. A one-of-one NFT or RWA uses the transaction CID as its metadata location. Example media response: ```json { "success": true, "data": { "name": "EXAMPLE", "cid": "bafy...", "series": 0, "image_available": true, "image_url": "/api/v1/nfts/image?name=EXAMPLE&series=0", "metadata": { "name": "Example NFT", "image": "ipfs://bafy.../image.png" } } } ``` The CID is always returned, including when the API serves its placeholder. Applications may therefore retrieve the original media through another IPFS gateway. `NFT_CACHE_PATH` must be writable by the PHP process and should remain outside the public web directory. Operators may serve the image route through a CDN if desired. Cached content is not committed to Git. The PHP API requires the cURL and GD extensions when local NFT caching is enabled. `IPFS_GATEWAY` is not used while `LOCAL_CACHE=NO`. ### Loans And Marketing | Method | Route | Purpose | | --- | --- | --- | | `GET` | `/api/v1/loans/by-hash?hash=...` | Loan by contract hash | | `GET` | `/api/v1/loans/by-address?address=...` | Loans involving an address | | `GET` | `/api/v1/loans/collateral?hash=...` | Loan collateral status | | `GET` | `/api/v1/marketing/history?advertiser=...&campaign=...&skip=0&limit=100` | Campaign records | ### Additional Address Routes | Method | Route | Purpose | | --- | --- | --- | | `GET` | `/api/v1/addresses/latest?address=...&limit=25` | Newest confirmed and pending activity | | `GET` | `/api/v1/addresses/vanity?address=...` | Vanity address registered to a canonical wallet | ### Storage Get a storage lookup quote: ```text GET /api/v1/storage/cost?storage_key=...&data_key=all&address=... ``` Along with the byte count, price, and payment address, the quote returns `rpc_endpoint`. This opaque configured endpoint ID must be submitted with the paid lookup: ```json { "storage_key": "64-character-storage-key-hash", "data_key": "all", "address": "40-character-address.cltc", "rpc_endpoint": "node-1", "payment_transaction_hex": "complete-signed-type-2-transfer" } ``` Send that JSON to: ```text POST /api/v1/storage/lookup ``` The API uses the same node that issued the quote. It intentionally does not fail over this request because another node may use a different payment address or lookup price. ### Governance ```text GET /api/v1/governance/proposals?proposal_key=... ``` This returns the node's proposal, vote, implementation, and activation state as JSON. The public API does not create wallet-registration signatures. A browser wallet must create and sign registration data locally before submitting it through `/api/v1/addresses/register`. The API never registers a wallet using credentials owned by the API operator.