Skip to content

Authentication

Pim Feltkamp edited this page Apr 27, 2026 · 1 revision

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).

Getting a token

  1. Sign in at cryptohopper.com and open the developer dashboard.
  2. Create an OAuth application — you'll receive a client_id (40 chars) and a client_secret.
  3. Drive the OAuth consent flow to receive a 40-character bearer token scoped to the permissions you requested:
    https://www.cryptohopper.com/oauth-consent?app_id=<client_id>&redirect_uri=<your_uri>&state=<csrf>
    
  4. Exchange the auth code at POST https://api.cryptohopper.com/v1/oauth/token (the SDK does not handle this exchange — it's a one-time setup step).

Pass the resulting bearer token as apiKey to the SDK:

val ch = Client.create("your-40-char-oauth-token")

For more options, use the builder:

val ch = Client.builder()
    .apiKey("your-40-char-oauth-token")
    .timeout(30, TimeUnit.SECONDS)
    .maxRetries(3)
    .build()

How the token reaches the server

The SDK sends the bearer in the access-token HTTP header (not Authorization: Bearer):

GET /v1/user/get HTTP/1.1
access-token: <your-40-char-oauth-token>
Accept: application/json
User-Agent: cryptohopper-sdk-kotlin/0.1.0-alpha.1

This is the contract the AWS API Gateway in front of the production API expects. Sending Authorization: Bearer instead returns 405 Missing Authentication Token from the gateway's SigV4 parser before the request ever reaches Cryptohopper. The legacy cryptohopper-android-sdk had this bug; this SDK gets it right from day one. See the live API docs for the authoritative reference.

Optional appKey

If you have a client_id (the OAuth app ID), pass it as appKey. The SDK sends it as the x-api-app-key header so the server can attribute traffic to your app and apply per-app rate limits separately from per-user limits.

val ch = Client.builder()
    .apiKey(System.getenv("CRYPTOHOPPER_TOKEN"))
    .appKey(System.getenv("CRYPTOHOPPER_APP_KEY"))
    .build()

This is optional but recommended for production traffic — without it you share rate-limit budget with every other unattributed caller.

Reading tokens from the environment

Standard CLI / server pattern:

val token = System.getenv("CRYPTOHOPPER_TOKEN")
if (token.isNullOrEmpty()) {
    System.err.println("Set CRYPTOHOPPER_TOKEN to a 40-char OAuth bearer first.")
    exitProcess(1)
}
val ch = Client.create(token)

For Android apps, never hard-code the token — store it in EncryptedSharedPreferences or the Android Keystore and read it at app launch:

import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

val prefs = EncryptedSharedPreferences.create(
    context,
    "cryptohopper_secure",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

val token = prefs.getString("oauth_bearer", null) ?: error("No token stored")
val ch = Client.create(token)

Empty token guard

Passing an empty apiKey raises IllegalArgumentException at construction time — it's a fail-fast guard so you don't silently issue unauthenticated requests:

try {
    Client.create("")
} catch (e: IllegalArgumentException) {
    // "apiKey must not be empty"
}

The same check fires from the builder: Client.builder().apiKey("").build() throws before any HTTP call is made.

Token rotation

OAuth tokens don't auto-refresh in the SDK — there's no refresh-token plumbing on the public client. When a long-lived token expires you'll get a CryptohopperError with code == "UNAUTHORIZED". Your app is responsible for re-running the consent flow (or retrieving a freshly-minted token from your backend) and constructing a new Client.

class TokenStore(initialToken: String) {
    @Volatile private var current: Client = Client.create(initialToken)

    fun client(): Client = current

    fun rotate(newToken: String) {
        current = Client.create(newToken)
    }
}

The reference itself is a Client instance — old in-flight requests on the previous instance complete normally; new requests go through the new client.

Per-IP authorization

If your OAuth app has IP whitelisting enabled, the server returns 403 FORBIDDEN from any other IP. The error includes the IP the server saw, which is the easiest way to debug:

try {
    ch.user.get()
} catch (e: CryptohopperError) {
    if (e.code == "FORBIDDEN") {
        e.ipAddress?.let { ip ->
            println("Server saw your IP as $ip. Add it to the OAuth app's whitelist on cryptohopper.com.")
        }
    }
}

Next steps

  • Error HandlingUNAUTHORIZED vs FORBIDDEN vs DEVICE_UNAUTHORIZED and how to recover
  • Rate Limits — per-app vs per-user buckets and what appKey buys you
  • Recipes — copyable patterns including retry wrappers that respect auth errors

Clone this wiki locally