Skip to main content
Your users authenticate to Etherfuse with JSON Web Tokens that you sign. You sign a short-lived JWT for a user, and we verify it against a JWKS URL you’ve registered with us. It’s an ordinary OAuth 2.0 JWT bearer exchange (RFC 7523), so any JWT library works. The simplest and recommended way to use a JWT is to launch the user straight into the Etherfuse app, where they complete flows like KYB. You can also exchange a JWT server-side to provision a user ahead of time or prefetch a refresh token that speeds up launch.

Before you start

1

Generate a signing key pair

Create an RSA key pair for RS256, the signing algorithm we verify. You sign JWTs with the private key and publish the public key (next step).
# RSA 2048
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
2

Host a JWKS endpoint

Publish the public key as a JSON Web Key Set (RFC 7517) at a stable HTTPS URL, e.g. https://your-domain.com/.well-known/jwks.json. Give the key a stable id (kid) so we can match it to a token’s kid header. Most JWT libraries can export a public key to JWK form:
{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256",
      "kid": "your-key-id",
      "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx...",
      "e": "AQAB"
    }
  ]
}
The URL must be reachable from Etherfuse’s servers over HTTPS and return the key set as JSON. We fetch it fresh on every verification (no caching), so rotating keys is straightforward: add the new key as another entry in keys, start signing with its kid, then remove the old key once no unexpired tokens still reference it.
3

Register your issuer with Etherfuse

Send your Etherfuse representative your issuer (iss) and JWKS URL. We link them to your partner organization so JWTs from that issuer resolve to your customers. Register separately for sandbox and production: each environment keeps its own issuer registration and uses its own aud (the sandbox vs production token endpoint). (There is no self-serve endpoint for this yet.)

Sign a user JWT

Sign a JWT with the key behind your JWKS. The claims:
ClaimRequiredNotes
issYour registered issuer.
subThe user’s id. A UUID is strongly recommended; see the note below.
audThe token endpoint URL: https://api.etherfuse.com/auth/token (or https://api.sand.etherfuse.com/auth/token in sandbox).
scopeRequired. Names the flow the user may complete. See Scopes.
jti / nonceA fresh value, unique per token, for replay protection. You can send it as jti (the standard JWT ID) or nonce (the standard OIDC claim).
email, nameThe user’s email and full name.
exp, iatKeep tokens short-lived (about 5 minutes). There is no enforced maximum lifetime, but verification allows no clock skew, so keep your server clock in sync.
pictureoptionalProfile image URL.
Strongly recommended: make sub a UUID. sub identifies the person signing in. For an individual customer, that person is the customer: use the same UUID for sub and for the customerId in the Ramp API, so both resolve to one customer. A non-UUID sub still signs in, but can’t be addressed through the Ramp API.
A sub must name a person, never a business. Don’t put a business organization’s id in sub: it’s rejected with invalid_grant. And don’t reuse a person’s sub as a business org id: POST /ramp/organization returns 409. A business is a separate org with its own id; a real person signs in with their own personal sub and completes the business’s verification on its behalf.
import jwt from "jsonwebtoken";
import { randomUUID } from "crypto";

// Reuse one UUID per user; it's their customerId in the Ramp API too.
const customerId = randomUUID();

const token = jwt.sign(
  {
    scope: "kyb",
    jti: randomUUID(),
    email: "[email protected]",
    name: "Ana García",
  },
  PRIVATE_KEY,                       // the key behind your JWKS
  {
    algorithm: "RS256",
    keyid: "your-key-id",            // matches a kid in your JWKS
    issuer: "https://your-domain.com",
    subject: customerId,
    audience: "https://api.sand.etherfuse.com/auth/token",
    expiresIn: "5m",
  },
);

Launch the user into the app

This is the recommended path. You hand the user’s browser to Etherfuse with their JWT, and the app signs them in and takes them where you point them (for example, KYB).
Launch lives on the app host, not the API host: https://app.etherfuse.com (https://sandbox.etherfuse.com in sandbox). It’s a browser page, not a JSON API.
Every launch carries:
  • grant_type and assertion: either a raw partner JWT (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) or a refresh_token from a prior server-side exchange (grant_type=refresh_token).
  • target: the app path to land on; see Targets for the available paths.
  • return_url (optional): where to send the user when they leave or the session can’t resume. It is not the success redirect; on success the user lands on target.
Have the user’s browser POST a form to /auth/launch, either as a top-level navigation or into an iframe you’ve embedded (point the form’s target at the frame). The app exchanges the JWT, establishes the session, and redirects to target. A server-rendered auto-submitting form is the usual way:
<form id="launch" method="POST" action="https://sandbox.etherfuse.com/auth/launch">
  <input type="hidden" name="grant_type" value="urn:ietf:params:oauth:grant-type:jwt-bearer" />
  <input type="hidden" name="assertion" value="<your_signed_jwt>" />
  <input type="hidden" name="target" value="/kyb" />
</form>
<script>document.getElementById("launch").submit()</script>
Rather not POST a form? Hand the credentials to the launch page over postMessage instead: open it in an iframe or popup and message the JWT in. See Launch via postMessage for the message contract and a working example.
Embedding. You can embed the launch page in an iframe or popup on your own site; there’s no origin to register with us. With postMessage, trust is established by the handshake itself: the page only accepts a reply from the parent or opener that requested it. Flows that use the camera, such as /idv, additionally need allow="camera *; microphone *" on the iframe element (the wildcard origin is required because the scan SDK uses the camera from a nested frame; a bare allow="camera" only covers the iframe’s own origin).

Scopes and targets

Which scope to sign and which target to launch into depends on the flow you’re sending the user into. Every available flow (and what to know about each) is listed in User Launch Flows.

When something fails

Auth failures come back as OAuth errors with an error, an error_description, and an error_uri pointing at the matching entry in Authentication errors. A launch renders these on the page rather than returning them. If you want to read them programmatically, exchange the JWT server-side first: POST /auth/token returns the same error as JSON, so you can branch on error before ever handing the user to a launch.