External Portfolio Read API
Authenticate, sign, and call the read-only external portfolio API to retrieve linked client metadata and portfolio snapshots.
The External Portfolio Read API exposes portfolio data for a partner team through three HMAC-protected, read-only endpoints. One API key set is scoped to a single team and can read every client account linked to that team.
This guide is partner-neutral: replace the example key identifiers and account ids with the credentials issued to your team.
Environment & base URL
| Environment | Base URL |
|---|---|
| Staging | https://www.staging.app.ice-block.net/api/v1/b2b |
| Production | https://app.iceblockinvestments.com/api/v1/b2b |
All requests must use HTTPS. The endpoints are:
GET /clientsPOST /portfolio/latestPOST /portfolio/history
Authentication & headers
Every request is authenticated with an HMAC-SHA256 signature computed from the request method, path, canonical query string, request body hash, and timestamp.
| Header | Required | Description |
|---|---|---|
x-iceblock-key | Yes | Public API key identifier issued to your team. |
x-timestamp | Yes | Unix timestamp in seconds (UTC). Used for replay protection. |
x-signature | Yes | sha256=<base64-encoded-hmac> computed from the string to sign. |
String to sign
METHOD PATHNAME CANONICAL_QUERY BODY_SHA256 TIMESTAMP
METHOD: uppercase HTTP method (GET,POST).PATHNAME: the exact request path, for example/api/v1/b2b/portfolio/latest.CANONICAL_QUERY: the normalized query string, or the empty string when there are no query parameters.BODY_SHA256: lowercase hexadecimal SHA-256 of the raw request body. ForGET, hash the empty string.TIMESTAMP: the same Unix timestamp sent inx-timestamp.
Canonical query rules
- Parse every query parameter from the URL.
- Sort parameters by key ascending.
- If a key appears multiple times, sort by value ascending.
- Percent-encode keys and values with
encodeURIComponent. - Join each pair as
<key>=<value>and concatenate them with&. - Use the empty string when there are no query parameters.
Replay protection
- Allowed clock skew is
±300seconds. - Requests outside this window return
403 Forbidden. - Missing or malformed timestamps return
400 Bad Request.
Signing example (Node.js)
const crypto = require('node:crypto');
function canonicalQuery(searchParams) {
return Array.from(searchParams.entries())
.sort(([keyA, valueA], [keyB, valueB]) => {
if (keyA === keyB) {
return valueA.localeCompare(valueB);
}
return keyA.localeCompare(keyB);
})
.map(
([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
)
.join('&');
}
function sha256Hex(input) {
return crypto.createHash('sha256').update(input).digest('hex');
}
function signRequest({ method, pathname, searchParams, body, timestamp, secret }) {
const rawBody = body ?? '';
const stringToSign = [
method.toUpperCase(),
pathname,
canonicalQuery(searchParams),
sha256Hex(rawBody),
String(timestamp),
].join('\n');
const hmac = crypto
.createHmac('sha256', secret)
.update(stringToSign)
.digest('base64');
return `sha256=${hmac}`;
}
Implementation notes
- For
GET, sign withbody = ''. - For
POST, sign the exact raw JSON string that will be sent on the wire. Do not reformat whitespace or reorder JSON keys between signing and sending. - If your HTTP client serializes JSON for you, hash the serialized string, not an in-memory object.
- Signature mismatches almost always come from hashing a parsed object instead of the raw body, signing one query-string order and sending another, or signing a different path than the one requested.
Common rules
- Monetary amounts and asset quantities are returned as strings to preserve decimal precision.
- UUIDs are strings; timestamps are ISO 8601 UTC; history dates are
YYYY-MM-DD. - One API key covers all client accounts linked to your team. An
account_uidthat exists but is not linked to your team returnsnot_authorized. account_uidsaccepts up to200UUIDs per request; duplicates are ignored after the first occurrence.- Borrow exposure is represented with a negative
value_usd.is_stablecoin = trueidentifies stablecoin positions. - In history responses, each daily entry represents the latest snapshot on or before the end of that UTC day;
is_carry_forward = truemeans an earlier snapshot was reused.
Endpoints
GET /clients
Returns every client account linked to your team with contact and technical custody metadata only. It does not return balances or holdings.
| Query parameter | Type | Required | Notes |
|---|---|---|---|
updated_since | string (ISO8601) | No | Returns only clients whose exposed metadata changed on or after the timestamp. |
include | string | No | Comma-separated values from contact,wallets,cefi_accounts. Defaults to all three. |
GET /api/v1/b2b/clients?include=contact,wallets HTTP/1.1 x-iceblock-key: your_key_id x-timestamp: 1775030400 x-signature: sha256=...
POST /portfolio/latest
Returns the latest available portfolio snapshot for each requested client account.
{
"account_uids": ["11111111-1111-4111-8111-111111111111"],
"include_positions": true
}
| Field | Type | Required | Notes |
|---|---|---|---|
account_uids | string[] | Yes | Up to 200 UUIDs. |
include_positions | boolean | No | Defaults to true. When false, the snapshot omits positions. |
Each result is classified as ok, not_found, not_authorized, or no_snapshot.
POST /portfolio/history
Returns daily portfolio history for a batch of client accounts over a requested date range.
{
"account_uids": ["11111111-1111-4111-8111-111111111111"],
"from": "2026-01-01",
"to": "2026-01-31",
"include_positions": false
}
| Field | Type | Required | Notes |
|---|---|---|---|
account_uids | string[] | Yes | Up to 200 UUIDs. |
from | string (YYYY-MM-DD) | Yes | Inclusive start date (UTC). |
to | string (YYYY-MM-DD) | Yes | Inclusive end date (UTC). |
include_positions | boolean | No | Defaults to false for history. |
Backfill longer ranges with multiple calls when the requested range exceeds the per-request history cap.
Error responses
| Status | Reason |
|---|---|
400 Bad Request | Missing headers, malformed JSON, invalid UUID, invalid timestamp, invalid date format, or invalid query parameter. |
401 Unauthorized | Invalid API key or signature mismatch. |
403 Forbidden | Timestamp outside the replay window, or disabled credentials. |
422 Unprocessable Entity | Semantically invalid request, such as from > to or more than 200 account_uids. |
429 Too Many Requests | Per-key rate limit exceeded, daily cap exceeded, or the rate limiter is temporarily unavailable. Honor the Retry-After header. |
500 Internal Server Error | Unexpected server failure. |
All errors use the same shape:
{
"error": {
"code": "invalid_signature",
"message": "Signature mismatch"
}
}
Usage metrics & audit traces (team managers)
Team owners and super-admins can review API activity on the team API access page (and on the admin account detail page for super-admins).
- Usage metrics are shown per key on the credentials card: last used, today's request count, and request count in the last 30 days. These are transient Redis snapshots with a 30-day inactivity TTL;
Neverand0mean no snapshot is currently retained. - Audit traces record credential and settings lifecycle events only (
credential.created,credential.rotated,settings.updated, the super-admin emergency disable, etc.). Operator email is stored denormalized when the event is written, so rows remain attributable even if the user account is later removed.
Per-request activity (route, HTTP status, latency) is not stored in the audit log or in permanent database counters — it is captured in server logs for debugging. No secrets or request bodies are recorded.
Rate limits & rotation
- Requests are rate limited per key (default 300 requests/minute, configurable per team). An optional daily request cap (UTC) is enforced when configured by IceBlock administrators. When a limit is exceeded — or if the rate limiter is briefly unavailable — the API returns
429with aRetry-Afterheader indicating when to retry. Error codes:rate_limit_exceeded,daily_limit_exceeded,rate_limit_unavailable. - API keys can be created, rotated, disabled, revoked, and re-enabled from the team API access page.
- The HMAC secret is shown only once, at creation or rotation. Store it securely and rotate it if it is ever exposed. Rotation requires confirmation, issues a replacement key, and immediately revokes the prior key so only one active credential remains.