Skip to main content
Version: 11.3.0

Writing Affirm Code Customizations

Overview

A code customization is a JavaScript program you register with HYPR Affirm and attach to a verification flow. Affirm runs it at a defined point in the flow — before the flow starts, when a notification is sent, or after the verification decision is made — so you can integrate an external system of record or define your own post-verification behavior.

Customizations execute in an ES2022 runtime inside Affirm. Browser APIs and Node.js APIs are not available.

This page covers how to write a customization. For the input and output contract of each customization type, see Code Customizations.

Choose a customization type

Use a customization when a standard integration cannot supply what your verification flow needs:

  • The user profile lives outside your IdP. A User Directory customization applies when an Okta or Entra ID integration is unavailable, or when it does not hold every profile field the flow requires — email, phone number, postal address, or a manager identifier. It also applies when your system of record keys users on an identifier such as an employee ID, which needs custom mapping to the value the requester types on the first screen.
  • The verification decision must drive an external action. An Outcome API Call customization runs after the decision, so you can write the result back to an external system, trigger a password reset, or return your own content to the requester.
  • Notifications must leave through your own gateway. SMS Sending, SMS Verifying, and Email customizations route the message through your REST gateway instead of HYPR's delivery services.
  • The flow needs a verification step HYPR does not provide. A Custom Verification Step registers your own single-page application as a step in the flow.

User Directory compared with Outcome

User Directory and Outcome customizations differ in when they run and what they are responsible for:

User DirectoryOutcome API Call
When it runsAt the start of the flow, when the requester submits the first screen. Runs a second time for the manager's profile when the flow requires an approver.Once, at the end of the flow, after the verification decision is determined.
What it returnsThe requester's (or manager's) profile fields, mapped to Affirm's parameter names.Whether the external call succeeded, and optionally content to display to the requester.
PurposeSupplies the profile data the verification steps compare against.Acts on the decision that has already been made.

Because a User Directory customization runs twice, branch on the isApprover input to return the right profile.

Inputs and attributes

A customization reads two kinds of values:

  • Inputs are supplied by the runtime for each execution, as a JSON string passed to the entry point. Each customization type has its own input contract — a User Directory customization receives loginIdentifier and isApprover; an Outcome customization receives loginIdentifier, email, isApproved, and workflowId.
  • Attributes are static key-value pairs an administrator configures alongside the script in the Control Center. Use them for values that belong to the environment rather than the code — API base URLs, tenant identifiers, and client credentials. Attribute values are stored encrypted.

Keeping credentials and URLs in attributes rather than in the script means the same customization can be promoted between environments without editing code.

The entry point

Every customization exposes a handle function. Affirm calls it with the input JSON for that execution:

function handle(inputJson) {
}

Parse the input and read the attributes you need. ctx.log writes to the customization's log output, and ctx.getAttribute reads a configured attribute by name:

function handle(inputJson) {
const input = JSON.parse(inputJson);
ctx.log("FINE", "Custom handler input=" + JSON.stringify(input));

const tokenUrl = ctx.getAttribute("DIRECTORY_TOKEN_URL");
const clientId = ctx.getAttribute("DIRECTORY_CLIENT_ID");
const clientSecret = ctx.getAttribute("DIRECTORY_CLIENT_SECRET");
}

The ctx object

Because there are no browser or Node.js APIs in the runtime, everything a customization needs to reach the outside world comes from ctx. Type ctx in the code editor to list the available methods and their exact signatures.

MethodPurpose
ctx.getInputAsJson()Returns this execution's input JSON, as an alternative to the inputJson argument.
ctx.getAttribute(name)Reads a configured attribute by name.
ctx.log(level, message)Writes to the customization's log output. Levels follow the standard names, such as FINE, INFO, and SEVERE.
ctx.httpGet(url, headers)Performs an HTTP GET and returns the response body.
ctx.httpPost(url, headers, requestBody)Performs an HTTP POST.
ctx.httpPut(url, headers, requestBody, timeoutSeconds)Performs an HTTP PUT. The timeout is optional.
ctx.httpPatch(url, headers, requestBody, timeoutSeconds)Performs an HTTP PATCH. The timeout is optional.
ctx.jwtCreateSignedJwt(…)Creates a signed JWT.
ctx.jwtDecodeJwt(jwt)Decodes a JWT without verifying its signature.
ctx.jwtVerify(jwt, publicKey)Verifies a JWT signature.
ctx.jwtDecodeVerify(…)Decodes and verifies a JWT.
ctx.uuid()Returns a random UUID.
ctx.sha256(value)Returns the SHA-256 digest of a string.
ctx.getHmacSHA256Signature(key, message)Returns an HMAC-SHA256 signature.
ctx.base64EncodeToString(value)Base64-encodes a string or byte array.
ctx.generateRandomPassword(minimumLength, minimumSpecialCharacters, requireMixedCaseAlphas, minimumNumbers, specialCharactersLimitList)Generates a password meeting a complexity policy. Useful in an Outcome customization that resets a credential.
ctx.get(key) and ctx.put(key, value, ttlInMillis)A short-lived key-value store scoped to code customizations, for carrying a value such as a cached access token between executions.
ctx.getConfigFromEnv(propertyName)Reads a server-side configuration property. Confirm with your HYPR representative before relying on this in a deployment.

Design patterns

User Directory customization

  1. Parse the inputs and assign them to local variables.
  2. Read the attributes you need, such as API URLs and credentials.
  3. Obtain a bearer token with ctx.httpPost, if your directory requires OAuth2.
  4. Call the directory API with ctx.httpGet to fetch the profile.
  5. Branch on isApprover so the requester lookup and the manager lookup each return the appropriate profile.
  6. Map the external fields onto Affirm's parameter names and return them.

Outcome customization

  1. Parse the inputs and assign them to local variables.
  2. Read the attributes you need.
  3. Obtain a bearer token with ctx.httpPost, if the target system requires OAuth2.
  4. Call the target API with the matching ctx.http* method to carry out the outcome.
  5. Return the result, and the text or HTML to display to the requester if the flow displays outcome content.

Worked example: a User Directory customization

This example builds a User Directory customization one piece at a time. Start with the entry point, the input parse, and the attributes:

function handle(inputJson) {
const input = JSON.parse(inputJson);
ctx.log("FINE", "Custom handler input=" + JSON.stringify(input));

const tenantId = ctx.getAttribute("DIRECTORY_TENANT_ID");
const clientId = ctx.getAttribute("DIRECTORY_CLIENT_ID");
const clientSecret = ctx.getAttribute("DIRECTORY_CLIENT_SECRET");
}

Branch on isApprover so you can treat the manager lookup differently from the requester lookup:

    if (input.isApprover) {
// resolve the approver's profile
} else {
// resolve the requester's profile
}

Move the token exchange and the directory query into their own functions, so handle stays readable. Both use ctx for the HTTP call — fetch and Node's HTTP modules are not available:

function getAccessToken() {
const body = "grant_type=client_credentials"
+ "&client_id=" + ctx.getAttribute("DIRECTORY_CLIENT_ID")
+ "&client_secret=" + ctx.getAttribute("DIRECTORY_CLIENT_SECRET");

const res = ctx.httpPost(
ctx.getAttribute("DIRECTORY_TOKEN_URL"),
{ "Content-Type": "application/x-www-form-urlencoded" },
body
);

return JSON.parse(res).access_token;
}

function getUserProfile(loginIdentifier, accessToken) {
const res = ctx.httpGet(
ctx.getAttribute("DIRECTORY_BASE_URL") + "/users/" + encodeURIComponent(loginIdentifier),
{ "Authorization": "Bearer " + accessToken, "Accept": "application/json" }
);

return JSON.parse(res);
}

ctx.httpGet and ctx.httpPost return the response body as a string, so parse it yourself. If the same access token can serve several executions, cache it with ctx.put and read it back with ctx.get rather than exchanging credentials every time.

Finally, map the directory's field names onto Affirm's parameter names and return the object. Affirm reads the profile from these keys, so the names must match the User Directory output contract exactly:

function handle(inputJson) {
const input = JSON.parse(inputJson);
ctx.log("FINE", "Custom handler input=" + JSON.stringify(input));

const tenantId = ctx.getAttribute("DIRECTORY_TENANT_ID");
const clientId = ctx.getAttribute("DIRECTORY_CLIENT_ID");
const clientSecret = ctx.getAttribute("DIRECTORY_CLIENT_SECRET");

const accessToken = getAccessToken();
const userProfile = getUserProfile(input.loginIdentifier, accessToken);

return {
loginIdentifier: input.loginIdentifier,
email: userProfile.mail,
firstName: userProfile.firstName,
lastName: userProfile.lastName,
mobilePhone: userProfile.mobilePhone,
streetAddress: userProfile.streetAddress,
city: userProfile.city,
state: userProfile.state,
postalCode: userProfile.zipCode,
countryCode: userProfile.countryCode,
status: "ACTIVE_FOR_AFFIRM",
managerLoginId: userProfile.managerId,
};
}

Return only the fields your verification flow needs. Fields the flow does not use may be omitted or returned as null — see Profile data each step requires to determine which apply.

Handling errors

Report a condition by what you return. A customization always returns a JavaScript object:

  • Return an empty object{} — when the record cannot be found in the directory.
  • Return an error message{ error: "My error message" } — to surface your own condition.
An uncaught exception is not a failure signal

If the script throws, the runtime records the error in the customization's logs and continues the verification flow with an empty result for that customization — the same as returning {}. It does not fail the step or select a failure outcome on your behalf. Handle your own error conditions and return an explicit result; do not rely on throwing to steer the flow.

In Test mode the behavior differs deliberately: an exception is surfaced to you as an error rather than swallowed, so you can see what went wrong before the customization goes near a live flow.

Returning null — or anything that is not an object — is treated as an error.

Per-step retry counts and failure outcomes are configured on the flow, independently of the customization; see Injectable Outcomes & Retry Limits.

Edit and test a customization

Customizations are managed in HYPR Affirm → Advanced Settings → Code Customizations. Select the customization from the drop-down to work on it.

  • Edit Mode — the code editor and the attribute list are read-only until you turn on Edit Mode. Turn it on to change the script or update attributes, then save.
  • Test — switch to Test to run the customization against values you supply, before you attach it to a live flow.

When you test a customization, confirm that:

  • each verification outcome your flow can produce is exercised, both approved and denied;
  • any redirect URL the customization returns resolves as intended;
  • every attribute your code reads with ctx.getAttribute is configured, and its name matches the code exactly.

Observability

Customization results are recorded alongside the rest of the verification flow:

  • The Activity Log records the customization's result as a first-class step result in the flow.
  • The Affirm Helpdesk presents the same verification history to service-desk operators, without the rest of the Control Center interface.
  • The Audit Trail records administrative actions, including attribute updates and changes to customization code.