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

EnvironmentBase URL
Staginghttps://www.staging.app.ice-block.net/api/v1/b2b
Productionhttps://app.iceblockinvestments.com/api/v1/b2b

All requests must use HTTPS. The endpoints are:

  • GET /clients
  • POST /portfolio/latest
  • POST /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.

HeaderRequiredDescription
x-iceblock-keyYesPublic API key identifier issued to your team.
x-timestampYesUnix timestamp in seconds (UTC). Used for replay protection.
x-signatureYessha256=<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. For GET, hash the empty string.
  • TIMESTAMP: the same Unix timestamp sent in x-timestamp.

Canonical query rules

  1. Parse every query parameter from the URL.
  2. Sort parameters by key ascending.
  3. If a key appears multiple times, sort by value ascending.
  4. Percent-encode keys and values with encodeURIComponent.
  5. Join each pair as <key>=<value> and concatenate them with &.
  6. Use the empty string when there are no query parameters.

Replay protection

  • Allowed clock skew is ±300 seconds.
  • 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 with body = ''.
  • 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_uid that exists but is not linked to your team returns not_authorized.
  • account_uids accepts up to 200 UUIDs per request; duplicates are ignored after the first occurrence.
  • Borrow exposure is represented with a negative value_usd. is_stablecoin = true identifies stablecoin positions.
  • In history responses, each daily entry represents the latest snapshot on or before the end of that UTC day; is_carry_forward = true means 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 parameterTypeRequiredNotes
updated_sincestring (ISO8601)NoReturns only clients whose exposed metadata changed on or after the timestamp.
includestringNoComma-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
}
FieldTypeRequiredNotes
account_uidsstring[]YesUp to 200 UUIDs.
include_positionsbooleanNoDefaults 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
}
FieldTypeRequiredNotes
account_uidsstring[]YesUp to 200 UUIDs.
fromstring (YYYY-MM-DD)YesInclusive start date (UTC).
tostring (YYYY-MM-DD)YesInclusive end date (UTC).
include_positionsbooleanNoDefaults to false for history.

Backfill longer ranges with multiple calls when the requested range exceeds the per-request history cap.

Error responses

StatusReason
400 Bad RequestMissing headers, malformed JSON, invalid UUID, invalid timestamp, invalid date format, or invalid query parameter.
401 UnauthorizedInvalid API key or signature mismatch.
403 ForbiddenTimestamp outside the replay window, or disabled credentials.
422 Unprocessable EntitySemantically invalid request, such as from > to or more than 200 account_uids.
429 Too Many RequestsPer-key rate limit exceeded, daily cap exceeded, or the rate limiter is temporarily unavailable. Honor the Retry-After header.
500 Internal Server ErrorUnexpected 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; Never and 0 mean 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 429 with a Retry-After header 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.