Authentication
All HippoDid API endpoints require authentication.
Table of contents
API Keys (recommended for server-to-server)
The simplest authentication method. Issue an API key from the HippoDid dashboard and include it as a Bearer token.
curl https://api.hippodid.com/v1/characters \
-H "Authorization: Bearer hd_key_YOUR_KEY_HERE"
API keys are scoped to your tenant. All keys from the same tenant share the same rate limits and character access.
| Tier | Max API keys |
|---|---|
| FREE | 1 |
| STARTER | 1 |
| DEVELOPER | 3 |
| BUSINESS | 10 |
API keys are secrets. Never commit them to source control. Use environment variables or a secrets manager.
JWT Authentication (for browser apps)
For browser-based applications, authenticate users via Clerk. The Clerk organization ID maps to a HippoDid tenant, and the user ID maps to a tenant member.
# Exchange a Clerk session token for a JWT, then use it as a Bearer token
curl https://api.hippodid.com/v1/characters \
-H "Authorization: Bearer eyJhbGciOiJS..."
Clerk → HippoDid mapping:
| Clerk concept | HippoDid concept |
|---|---|
Organization ID (org_...) | Tenant ID |
User ID (user_...) | Member ID |
Organization role (org:admin) | ADMIN member role |
Organization role (org:member) | MEMBER member role |
Member roles and permissions:
| Role | Permissions |
|---|---|
OWNER | Full access, billing, member management |
ADMIN | Manage characters and memories for all members |
MEMBER | Manage own characters and memories |
VIEWER | Read-only access (Developer+ tier) |
Spring Security integration
WebClient with API key (server-to-server)
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class HippoDidClientConfig {
@Value("${hippodid.api-key}")
private String apiKey;
@Bean
public WebClient hippoDidClient() {
return WebClient.builder()
.baseUrl("https://api.hippodid.com")
.defaultHeader("Authorization", "Bearer " + apiKey)
.build();
}
}
RestClient with API key
import org.springframework.web.client.RestClient;
@Bean
public RestClient hippoDidRestClient(@Value("${hippodid.api-key}") String apiKey) {
return RestClient.builder()
.baseUrl("https://api.hippodid.com")
.defaultHeader("Authorization", "Bearer " + apiKey)
.build();
}
Handling 401 and 403
var response = restClient.post()
.uri("/v1/characters/{id}/memories", characterId)
.body(request)
.retrieve()
.onStatus(status -> status.value() == 401, (req, res) -> {
throw new RuntimeException("Invalid or expired API key");
})
.onStatus(status -> status.value() == 403, (req, res) -> {
throw new RuntimeException("Access denied — check member role and tier");
})
.body(MemoryResponse.class);
Rate limit headers
Every API response includes rate limit information in the response headers.
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window (per minute) |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp (seconds) when the window resets |
X-RateLimit-Bucket | The rate limit bucket for this request: SEARCH, WRITE, SYNC, MANAGEMENT, or AI_PROXY |
Example response headers:
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 28
X-RateLimit-Reset: 1705312260
X-RateLimit-Bucket: WRITE
Handling 429 Too Many Requests
var response = restClient.post()
.uri("/v1/characters/{id}/memories", characterId)
.body(request)
.retrieve()
.onStatus(status -> status.value() == 429, (req, res) -> {
String resetHeader = res.getHeaders().getFirst("X-RateLimit-Reset");
long resetAt = Long.parseLong(resetHeader != null ? resetHeader : "0");
long waitMs = (resetAt * 1000L) - System.currentTimeMillis();
throw new RateLimitException("Rate limit exceeded, retry after " + waitMs + "ms");
})
.body(MemoryResponse.class);
For details on per-tier rate limits and bucket definitions, see Tiers & Limits.