Browser SDK

Use publish() for a save button: the user approves on HashBin and your app never holds a credential. Use an allowance with authorize() and save() for silent saves. Choose a safe flow · Upgrade an existing client · API contracts.

1. Load and configure

The immutable v0.2.0 release has no mutable module dependencies. This tag enforces Subresource Integrity and exposes window.hashbin:

<script type="module" src="https://hashbin.org/sdk/v0.2.0/hashbin.js"
  integrity="sha384-mB0YKaK9+BVa8ALdgsr2t2s8ZmFuY9DpcrvITMPb3+CZQMIQrPq+E5zBdLMYp1dX"
  crossorigin="anonymous"></script>

Use window.hashbin.createClient() from a following module script after the SDK loads. Pinning requires deliberate upgrades for fixes. For automatic updates, import the latest module instead:

import { createClient } from 'https://hashbin.org/sdk/hashbin.js';

const client = createClient({
  clientId: 'YOUR_CLIENT_ID',
  redirectUri: 'https://your-app.example/callback'
});

Register the exact redirect URI at Developers. The default API host is https://hashbin.org, even on your own site; content defaults to https://256t.us. Set baseUrl and contentBaseUrl explicitly for local tests. A plain import of a versioned URL pins its path but does not enforce SRI.

2. Publish with per-save approval

// Call directly from a click handler, before awaiting other work.
saveButton.onclick = async () => {
  const result = await client.publish(editor.value, {
    contentType: 'text/plain',
    clientState: { draft: editor.value }
  });
  if (!result.redirected) showSavedCID(result.cid);
};

HashBin shows the registered app, a safe text preview, CID, size, retention, exact price and balance. The user signs in and approves there. The five-minute approval is single-use and bound to the exact bytes, content type, user, app, retention and charge. Your client receives a verified CID, not tokens or the approval ticket.

ModeLimit and behavior
popup (default)1 byte to 5 MiB. Origin, opener and random state checks protect the handoff. A blocked popup falls back to a redirect for documents up to 512 KiB; larger documents require enabling popups and retrying.
redirectUp to 512 KiB. Data travels in the URL fragment, never in a server request. The SDK preserves bytes and optional clientState in session storage. It returns {redirected: true, state} while navigating.

For a client using Cross-Origin-Opener-Policy: same-origin, choose redirect mode, or use same-origin-allow-popups where appropriate. A lost opener connection does not silently retry a paid save. A rejected, expired or reused approval needs a new review.

Handle a publish redirect

Create the same client on the registered callback page, then verify the saved transaction before restoring your editor state:

if (new URLSearchParams(location.hash.slice(1)).has('hashbin_publish')) {
  const { cid, clientState } = await client.handlePublishCallback();
  restoreEditorState(clientState); // Your application's own restore function.
  showSavedCID(cid);
  history.replaceState(null, '', location.pathname + location.search);
}

3. Opt into an allowance for silent saves

Connect only when your app needs delegated operations without approval for each save. authorize() defaults to popup mode and opens the allowance entry page, then HashBin consent. The user can remove permissions, lower limits and choose expiry.

connectButton.onclick = async () => {
  await client.authorize({
    mode: 'popup',
    scopes: ['content:write', 'balance:read'],
    spendingLimit: 1,       // US dollars per month; SDK default
    maxUploadBytes: 1048576, // 1 MiB; SDK default
    maxRetentionMonths: 3
  });
};

Consent defaults to 30 days and offers 1, 7, 30, 90 or 365 days, or no grant expiry. Spending scopes always require a monthly cap. SDK options suggest spending, file-size and retention ceilings; the expiry is chosen on HashBin.

Complete an OAuth callback

This is separate from the publish redirect callback. On your registered OAuth callback page, load the SDK and create the same client, then:

await client.handleCallback({
  postMessageToOpener: Boolean(window.opener),
  closeWindow: Boolean(window.opener)
});

For a popup, this sends the code and state to the original window, which performs the PKCE exchange with its key. For explicit redirect authorization, the callback exchanges the code itself. Redirect authorization requires persistent IndexedDB key storage and session storage. If the SDK falls back to a memory-only key, use popup authorization and sign in again next session.

Save, read balance and disconnect

const balance = await client.getBalance();
const saved = await client.save(editor.value, {
  contentType: 'text/plain',
  maxCostCents: 1
});
console.log(balance.balance_cents, saved.cid);
await client.disconnect(); // Server-side grant revocation, then local removal.

OAuth uploads use the account's default retention narrowed by the grant's retention cap. maxCostCents is a non-negative safe integer in cents, separate from the cumulative monthly cap. A higher upload charge returns 409 max_cost_exceeded before debit/storage, including duplicate-content extensions; invalid ceilings return 400.

4. Verify retrieved content

const response = await client.retrieve(saved.cid);
const bytes = await response.arrayBuffer(); // Already verified before retrieve() returns.

// For a custom fetch or cache:
const valid = await hashbin.verifyCID(saved.cid, bytes);
if (!valid) throw new Error('Content does not match its CID');

retrieve() reads the complete body, verifies the length prefix and inline bytes up to 64 bytes or SHA-512 above that, then returns a Response. It does not stream unverified content. publish() and save() verify their returned CID. verifyCID(cid, content) accepts strings, Blobs, ArrayBuffers and typed-array views; a false result means the bytes must not be used.

Storage, lifetime and recovery

  • New SDK authorizations bind access and refresh tokens to a non-extractable P-256 key in IndexedDB. DPoP proofs cover exchange, refresh and authenticated API requests; funded OAuth requests on the content worker enforce the same binding.
  • Allowance tokens normally live in localStorage. DPoP prevents using a copied token alone on another device. A script already running on the same origin can use the browser key. Prefer per-save approval on shared origins.
  • Access tokens last 15 minutes. New refresh tokens expire after seven idle days and a family lasts at most 90 days despite rotation. Grant expiry can shorten access. Existing unbound tokens keep their existing expiry and are not automatically device-bound.
  • Refresh requires client_id. Reusing a rotated refresh token revokes the whole grant. The SDK shares renewal within a client and uses Web Locks plus a storage reread across tabs. Without Web Locks, use one active allowance tab or per-save approval.
  • disconnect() revokes remote grant access. clearTokens() removes only local copies. The account page shows activity/spend and in-app security notifications, and can revoke all app access. Revoke-all does not revoke API keys.

Raw OAuth clients without DPoP still receive bearer tokens. The bundled native and command-line examples use that flow; native secure storage does not add sender constraint automatically. See the API reference for custom proof fields, server nonces and recovery endpoints.

Migrate from the previous SDK

  1. Save-button-only app: remove OAuth setup and use the new publish(), handling handlePublishCallback() for redirects.
  2. Keep silent saves: replace the former OAuth-upload publish() call with save(), keep authorization, use the updated SDK for DPoP, and set maxCostCents.
  3. Raw token client: include client_id on refresh, serialize renewals, and implement DPoP separately if needed.
  4. Custom fetches: call verifyCID() before using returned bytes or CIDs. Direct authorizedFetch() calls do not automatically verify content or add a price ceiling.
  5. Disconnect and hosting: revoke server-side, review sibling apps on shared origins, and upgrade pinned releases deliberately for security fixes.

The browser Textpad walkthrough demonstrates a persistent allowance and direct API calls. It is not a migration of that example to credential-free publishing.