Documentation
End-to-end flow
One signup, complete: verify, persist eventId, challenge, solve, re-verify, then label the outcome.
The sequence
1. Verify at the action with a high-entropy, preferably server-generated session and an Idempotency-Key you generate, so retries of the same attempt return one verdict.
2. Persist `eventId` immediately on the pending signup / account row. Feedback joins only on that key: do not wait for allow.
3. When `decision` is `challenge`, or `degraded` is true, return HTTP 428, then issue friction with prefer in economic order: payment_preauth, proof_of_work, web_bot_auth_stepup.
4. Prove friction was satisfied (complete with challengeId + proof), then check the result. Completion clears session state only: it does not authorize the action. Expired (410 challenge_expired) or invalid proofs must not create the account: issue a fresh challenge. Replays return 409; a different session returns 403.
5. Re-verify with context.challengeId and a new idempotency key. Proceed only on non-degraded allow. When the clear is honored, challengeCleared: true is set and scoring is short-circuited to allow (never challenge + challengeCleared).
6. Create the account only on an explicit non-degraded `allow` (decision === "allow" and degraded === false). Deny stops; any other verdict stays on the challenge response (HTTP 428) and POST /v1/challenge flow.
7. Label the outcome when it lands: credit burn from usage metering, chargebacks from billing, abuse confirmations from your trust queue, see the outcome-label connectors guide.
Full code
Complete loop for one signup. Subject fields stay hashed by default (piiMode: "hashed"). solvePow / solve_pow is your own solver: hash candidate nonces with the issued seed until the hex digest starts with the required zero nibbles, see the tip below.
import { ChitmarkApiError } from "@chitmark/sdk";
const session = `sess_${crypto.randomUUID()}`;
const idempotencyKey = `signup:${session}`;
// 1. Verify at the action (email/ip/userAgent hashed client-side by default)
let verdict = await chitmark.verify({
action: "signup",
session,
idempotencyKey, // required for safe retries of this attempt
subject: { email, ip, userAgent },
});
// 2. Persist the join key immediately (before branching)
let eventId = verdict.eventId;
await db.signups.stashEventId({ session, eventId });
// 3. Challenge (or degraded): issue, solve, complete with explicit failure handling
if (verdict.decision === "challenge" || verdict.degraded) {
const ch = await chitmark.challenge({
eventId,
session,
prefer: ["payment_preauth", "proof_of_work"],
});
// Branch on ch.method: unavailable prefer entries fall back to proof_of_work.
try {
if (ch.method !== "proof_of_work") {
throw new ChallengeRequiredError(); // route to your payment_preauth UI
}
const nonce = await solvePow(ch.instructions);
const cleared = await chitmark.completeChallenge({
eventId,
challengeId: ch.challengeId,
session,
proof: { type: "proof_of_work", nonce },
});
if (!cleared.ok) {
throw new ChallengeRequiredError(); // do not treat a soft failure as allow
}
} catch (err) {
if (
err instanceof ChitmarkApiError &&
(err.code === "challenge_expired" ||
err.code === "invalid_challenge_proof" ||
err.code === "challenge_method_mismatch")
) {
throw new ChallengeRequiredError(); // issue a fresh challenge; never create
}
throw err;
}
// 4. Re-verify so the next verdict honors the cleared session
verdict = await chitmark.verify({
action: "signup",
session,
idempotencyKey: `${idempotencyKey}:cleared`,
context: { challengeId: ch.challengeId },
subject: { email, ip, userAgent },
});
eventId = verdict.eventId;
await db.signups.stashEventId({ session, eventId });
// verdict.challengeCleared === true
}
// 5. Only an explicit, non-degraded allow creates the account
if (verdict.decision === "deny") {
throw new BlockedError();
}
if (verdict.decision !== "allow" || verdict.degraded) {
throw new ChallengeRequiredError();
}
const account = await db.accounts.create({ email, chitmarkEventId: eventId });
// ... later, when the outcome lands:
// 6. Label it, keyed by the stored event id
await chitmark.feedback({
eventId: account.chitmarkEventId,
outcome: "credit_burn",
value: 12.4, // economic magnitude: burn $, chargeback $, or converted first-year ARR
unit: "usd", // scale for value: usd | credits | count (default: usd)
});Challenge issues and completions are two calls on the same verb, see the challenge API reference: issue with no proof, complete with challengeId and proof.