Skip to content

Authentication

Pim Feltkamp edited this page Apr 27, 2026 · 3 revisions

Authentication

Every SDK request requires an OAuth2 bearer token (the AWS API Gateway in front of the production API rejects every unauthenticated call as of today, including conceptually-"public" market-data routes):

access-token: <40-char token>

Obtaining a token

  1. Log in to cryptohopper.com.
  2. Developer → Create App — gives you a client_id + client_secret.
  3. Complete the OAuth consent flow for your app, which returns a bearer token.

Options to automate step 3:

  • The official CLI: cryptohopper login opens the consent page, runs a loopback listener, and persists the token to ~/.cryptohopper/config.json. Read that JSON from PHP and pull out token.
  • Your own code: call the server's /oauth2/authorize + /oauth2/token endpoints directly. The CLI's implementation is short (~300 lines of TypeScript) and a reasonable reference.

Client construction

use Cryptohopper\Sdk\Client;

$client = new Client(
    apiKey:     getenv('CRYPTOHOPPER_TOKEN'),
    appKey:     getenv('CRYPTOHOPPER_APP_KEY') ?: null,
    baseUrl:    'https://api.cryptohopper.com/v1',
    timeout:    30,
    maxRetries: 3,
    userAgent:  'my-app/1.0',
);

Only apiKey: is required.

appKey:

Cryptohopper lets OAuth apps identify themselves on every request via the x-api-app-key header (value = your OAuth client_id). When set, the SDK adds the header automatically. Reasons to set it:

  • Shows up in Cryptohopper's server-side telemetry — you can attribute your own traffic.
  • Drives per-app rate limits — if two apps share a token, they get independent quotas.
  • Harmless to omit; the server accepts unattributed requests.

The SDK treats empty strings as "not set," so passing getenv('CRYPTOHOPPER_APP_KEY') ?: null is safe even when the env var is unset.

baseUrl:

Override for staging or a local dev server. The default is https://api.cryptohopper.com/v1. The trailing /v1 is part of the base; resource paths are relative to it.

$client = new Client(
    apiKey: $token,
    baseUrl: 'https://api.staging.cryptohopper.com/v1',
);

httpClient: — bring your own Guzzle client

If you need custom transport behaviour — proxies, custom CA bundles, connection-pool tuning, middleware (logging, tracing, request signing) — pass your own GuzzleHttp\ClientInterface:

use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;

$stack = HandlerStack::create();

// Add logging middleware
$stack->push(Middleware::log(
    $logger,
    new \GuzzleHttp\MessageFormatter('{method} {uri} → {code} {res_header_X-RateLimit-Remaining}')
));

// Add a request-signing middleware (custom; see Guzzle docs)
$stack->push(MyRequestSigner::middleware($signingKey));

$guzzle = new GuzzleClient([
    'handler' => $stack,
    'http_errors' => false,                   // SDK already handles non-2xx; keep this off
    'verify' => '/path/to/corp-ca-bundle.pem', // corporate root CA
    'proxy' => 'http://corp-proxy:3128',
    'connect_timeout' => 5,                    // separate from total timeout
]);

$client = new Client(
    apiKey: $token,
    httpClient: $guzzle,
);

The SDK doesn't override your client's options — set them as you need them. The SDK's per-request timeout: argument is passed via Guzzle's ['timeout' => ...] request option, which works alongside any defaults you set on the client.

timeout: and maxRetries:

timeout: is the per-request total timeout in seconds (Guzzle's timeout option, which maps to CURLOPT_TIMEOUT — covers the entire request including body). Defaults to 30.

maxRetries: is the number of automatic retries on HTTP 429. Default 3. Set to 0 to disable. See Rate Limits.

userAgent:

Appended after the SDK's own User-Agent (cryptohopper-sdk-php/<version>). Set this to identify your client to Cryptohopper support if you ever need to debug something with them.

IP allowlisting

If your Cryptohopper app has IP allowlisting enabled, requests from unlisted IPs return 403 FORBIDDEN. The SDK surfaces this as CryptohopperException with getErrorCode() === 'FORBIDDEN' and getIpAddress() populated:

catch (CryptohopperException $e) {
    if ($e->getErrorCode() === 'FORBIDDEN') {
        $logger->warning('Cryptohopper blocked us', ['ip' => $e->getIpAddress()]);
    }
}

For CI where the runner IP isn't stable, either disable IP allowlisting for that app or route outbound traffic through a stable IP (NAT gateway, dedicated proxy).

Rotating tokens

Cryptohopper bearer tokens are long-lived but can be revoked:

  • Manually from the dashboard.
  • When the user revokes consent.

The SDK surfaces revocation as UNAUTHORIZED on the next call. There is no automatic refresh-token handling in the SDK today — if your app uses refresh tokens, handle the UNAUTHORIZED branch by exchanging your refresh token for a new access token and constructing a fresh client. Wrap with a service-class to keep callers clean:

final class CryptohopperGateway {
    private Client $client;

    public function __construct(
        private readonly TokenStore $tokens,
    ) {
        $this->client = $this->build();
    }

    public function call(callable $fn): mixed {
        try {
            return $fn($this->client);
        } catch (CryptohopperException $e) {
            if ($e->getErrorCode() !== 'UNAUTHORIZED') {
                throw $e;
            }
            $this->tokens->refresh();
            $this->client = $this->build();
            return $fn($this->client); // single retry with fresh client
        }
    }

    private function build(): Client {
        return new Client(apiKey: $this->tokens->current());
    }
}

The Client is cheap to construct — it's primarily a coordinator for resource classes plus a Guzzle reference. Constructing a fresh one on token refresh is the right call.

Concurrency

Client is safe to share across processes/threads in standard PHP setups (PHP-FPM, Apache+mod_php) where each request gets its own process. Inside a single PHP process, it's not strictly thread-safe — but PHP's threading model means you typically don't need it to be.

For concurrent outbound calls within a single request, Guzzle's async pool primitives work directly:

use GuzzleHttp\Promise;
use GuzzleHttp\Pool;

// Build many promises in parallel, bounded at 5 concurrent
$promises = function () use ($hopperIds, $client) {
    foreach ($hopperIds as $id) {
        yield function () use ($client, $id) {
            // Wrap a sync call in an async promise
            return Promise\Coroutine::of(function () use ($client, $id) {
                yield $client->hoppers->get($id);
            });
        };
    }
};

In practice, most PHP apps keep things synchronous. See Rate Limits for capping concurrency at the API quota.

Authentication is required on every endpoint

The AWS API Gateway in front of the production API rejects every call without a valid OAuth bearer token (returns 405 Missing Authentication Token). This holds even on routes the API conceptually treats as "public market data" like /exchange/ticker and /market/homepage. There is no way to call the SDK without a real token today.

Clone this wiki locally