Getting started

Integrate with AI coding assistants

Instructions to give an AI coding assistant so it integrates isCredible correctly: where to call it, what to send, and the rules to follow.

Integrate isCredible: instructions for AI coding assistants

This page is written for AI coding assistants (Claude Code, Cursor, GitHub Copilot, Windsurf and similar) and for the developers who use them. Give your assistant this page, or its plain-text copy at https://iscredible.com/ai-integration.md, and ask it to integrate isCredible. It describes what to build, where, and the rules to follow.

What isCredible is#

isCredible is an HTTP API that checks a user action (a signup, login, trial start, form submission or expensive action) and returns one decision:

  • APPROVE: nothing worrying. Continue.
  • REVIEW: some risk, not conclusive. Add a light step (email or phone code, smaller free allowance) or flag it.
  • DENY: strong signs of abuse. Block, with a neutral message.

Each response also has a risk score from 0 to 100 and plain-language reasons. There is one endpoint for checks: POST https://api.iscredible.com/v1/credibility/verify.

Rules for the assistant#

Follow these in every integration.

  1. Call isCredible only from server-side code. Never from browser, mobile or desktop client code. The API key must never reach a client.
  2. Read the key from an environment variable named ISCREDIBLE_API_KEY. Never hard-code it, never commit it, never log it. Add it to .env.example with an empty value and to the deployment's secret settings.
  3. Fail open. If isCredible times out, is unreachable or returns a 5xx or 429, continue as if the decision were APPROVE and log the error. Use a short timeout (about 3 seconds) and at most one retry on user-facing paths.
  4. Check at events, not on every request. Signups, logins, trial starts, form submissions, card additions and expensive actions. Do not call it on page views or for every API request; for metered usage use the SDK's checkUsage, which caches decisions.
  5. Send only what the app already has. Never send passwords, full card numbers, CVV codes or government ID numbers. For cards, send the payment processor's card fingerprint (for Stripe, payment_method.card.fingerprint).
  6. Keep reasons internal. Show end users a neutral message such as "We couldn't create your account. Contact support if this is a mistake." Log requestId and the reasons for the team.
  7. Do not change the app's own security. isCredible adds a decision; it does not replace authentication, rate limiting, input validation or payment checks.
  8. Ask the developer before changing user-facing behaviour that is not described here, such as blocking existing users or deleting accounts.

Install#

Use the official SDK when the backend is JavaScript/TypeScript or Python. Otherwise, call the HTTP API directly with any HTTP client.

# Node.js 18+, serverless and edge runtimes (no dependencies)
npm install is-credible

# Python 3.9+ (no dependencies)
pip install is-credible

Create one client at startup and reuse it.

// server/iscredible.ts
import { IsCredibleClient } from "is-credible";

export const iscredible = new IsCredibleClient({
  apiKey: process.env.ISCREDIBLE_API_KEY!,
  timeoutMs: 3000,
  maxRetries: 1
});
# app/iscredible_client.py
import os
from iscredible import IsCredible

iscredible = IsCredible(api_key=os.environ["ISCREDIBLE_API_KEY"], timeout=3, max_retries=1)

Where to integrate#

Find these points in the codebase. Integrate the ones that exist; skip the rest. If the app has none of them, ask the developer what they want to protect.

Integration pointFind it by looking forFields to send
Signup / registrationthe handler that creates the user recordemail, ip, userAgent, deviceId (if available), accountCreation: true
Loginthe handler that runs after the password or OAuth check succeedsuserId, email, ip, userAgent, deviceId, loginAttempts
Free trial start / card addedtrial creation, subscription creation, payment-method webhooksuserId, email, ip, deviceId, paymentFingerprint, accountCreation: true
Public form (application, order, lead, contact, waitlist, review)the handler that stores a submission or emails the teameventType: "submission", email, phone, ip, userAgent, headers, timeToSubmitMs
Expensive or metered action (AI request, export, credits)code that calls an LLM, spends credits or checks a quotause checkUsage with the user's current usage

Getting the client IP: use the framework's trusted client IP (for example req.ip in Express with trust proxy set, request.client.host in FastAPI, x-forwarded-for set by your own load balancer). Do not trust a client-supplied IP header without a proxy you control.

A device ID is optional but makes repeat-account detection much stronger. If the app has a browser frontend, create a random ID on first visit, keep it in localStorage, and send it to the backend with signup and login requests:

// browser
export function getDeviceId(): string {
  let id = localStorage.getItem("device_id");
  if (!id) {
    id = crypto.randomUUID();
    localStorage.setItem("device_id", id);
  }
  return id;
}

Signup#

Check before creating the user record and before sending any email.

import { decide } from "is-credible";
import { iscredible } from "./iscredible";

export async function signup(req, res) {
  const { email, deviceId } = req.body;

  let action = "APPROVE";
  try {
    const result = await iscredible.verify({
      email,
      ip: req.ip,
      userAgent: req.get("user-agent"),
      deviceId,
      accountCreation: true
    });
    action = decide(result).action;
    if (action !== "APPROVE") log.info("isCredible", { requestId: result.requestId, action, reasons: decide(result).reasons });
  } catch (error) {
    log.warn("isCredible unavailable, allowing signup", { error }); // fail open
  }

  if (action === "DENY") {
    return res.status(403).json({ error: "We couldn't create your account. Contact support if this is a mistake." });
  }
  const user = await createUser({ email, needsVerification: action === "REVIEW" });
  return res.status(201).json({ id: user.id });
}
from iscredible import IsCredibleError, decide
from .iscredible_client import iscredible

def signup(request):
    data = request.json
    action = "APPROVE"
    try:
        result = iscredible.verify({
            "email": data["email"],
            "ip": client_ip(request),
            "userAgent": request.headers.get("User-Agent"),
            "deviceId": data.get("deviceId"),
            "accountCreation": True,
        })
        action, reasons = decide(result)
        if action != "APPROVE":
            log.info("isCredible %s %s %s", result["requestId"], action, reasons)
    except (IsCredibleError, ValueError) as error:
        # IsCredibleError: API unavailable or rejected the call. ValueError: malformed input, e.g. an invalid email.
        log.warning("isCredible check skipped, allowing signup: %s", error)  # fail open

    if action == "DENY":
        return {"error": "We couldn't create your account. Contact support if this is a mistake."}, 403
    user = create_user(data["email"], needs_verification=(action == "REVIEW"))
    return {"id": user.id}, 201

What REVIEW should do at signup: create the account but hold back the valuable part, such as requiring email or phone verification before free credits are granted.

Login#

Call after the credentials check succeeds. Always send userId: per-user history only works with a stable ID.

const result = await iscredible.verify({
  userId: user.id,
  email: user.email,
  ip: req.ip,
  userAgent: req.get("user-agent"),
  deviceId: req.body.deviceId,
  loginAttempts: failedAttemptsBeforeThisLogin
});
// APPROVE: continue. REVIEW: ask for a second factor or email a confirmation link.
// DENY: end the session, notify the account owner, require a password reset.

Form submissions#

Use the built-in Submission Screening scenario. No browser script or captcha is needed.

const result = await iscredible.verify(
  {
    eventType: "submission",
    email: form.email,
    phone: form.phone,
    ip: req.ip,
    userAgent: req.get("user-agent"),
    headers: { accept: req.get("accept"), "accept-language": req.get("accept-language") },
    timeToSubmitMs: Date.now() - Number(form.renderedAt) // optional: render the time into a hidden field when you show the form
  },
  { scenarioId: "SYS_SUBMISSION_SCREENING_001" }
);
// APPROVE: save. REVIEW: ask for an email or phone code first, or save it flagged.
// DENY: return the normal success response but do not save or forward it.

For applications such as credit or insurance, use the result to screen out bots and fake submissions, never to decide eligibility.

Metered usage (AI tokens, API calls, credits)#

The app keeps counting usage and enforcing its own limits. Before an expensive action, pass the user's current total to checkUsage. The SDK only calls the API when a check is due, so calling it on every expensive request is fine.

const { action } = await iscredible.checkUsage(
  user.id,
  { metric: "ai_tokens", used: tokensUsedThisPeriod, limit: user.planTokenLimit },
  { deviceId, ip: req.ip, accountCreatedAt: user.createdAt }
);
if (action === "DENY") return res.status(429).json({ error: "Free limit reached. Upgrade to continue." });
if (action === "REVIEW") { /* for example: require phone verification before more free usage */ }
decision = iscredible.check_usage(
    user.id,
    {"metric": "ai_tokens", "used": tokens_used, "limit": plan_token_limit},
    {"deviceId": device_id, "ip": ip, "accountCreatedAt": user.created_at.isoformat()},
)
if decision["action"] == "DENY":
    return {"error": "Free limit reached. Upgrade to continue."}, 429

Metric names are lower-case, such as ai_tokens, api_calls, credits, exports. Wrap the call in the same fail-open handling as above.

Without an SDK (any language)#

curl https://api.iscredible.com/v1/credibility/verify \
  -H "x-api-key: $ISCREDIBLE_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "userData": { "email": "jane@example.com", "ip": "203.0.113.7", "accountCreation": true } }'

Request body:

FieldTypeNotes
userDataobjectThe data to check. Unknown fields are ignored; missing fields just skip the checks that need them.
scenarioIdstring, optionalRun one scenario, e.g. SYS_SUBMISSION_SCREENING_001.
scenarioIdsstring[], optionalRun several scenarios.

Without scenarioId, all of the account's active scenarios run; an account with no active scenarios of its own uses the built-in set. When a specific built-in scenario is wanted, pass its scenarioId explicitly.

Response (abridged):

{
  "requestId": "6f3c8a6e-2b8e-4b6b-9b0e-7c1a5d2f9e10",
  "overallRecommendation": "REVIEW",
  "overallRiskScore": 50,
  "summary": "Needs review (50.0). ...",
  "scenarioResults": [
    {
      "scenarioId": "SYS_SUBMISSION_SCREENING_001",
      "recommendation": "REVIEW",
      "ruleReports": [
        { "ruleId": "...", "status": "FLAGGED", "findings": [{ "code": "DISPOSABLE_EMAIL_DETECTED", "message": "..." }] }
      ]
    }
  ]
}

Branch on overallRecommendation. Use overallRiskScore to choose a REVIEW step (lower score: a light check; higher: a closer look). Finding codes are stable for logging and metrics; messages are for people.

Errors#

StatusMeaningWhat the code should do
200Decision returnedUse overallRecommendation
400Invalid request bodyFix the request; errors lists the problems. Do not retry.
401 / 403API key missing, unknown or revokedLog loudly; fail open for the user; alert the developer
404Unknown scenarioIdFix the ID
429Rate limit or daily quota reachedFail open; retry later
5xx, timeoutTemporary problemFail open; log

The SDKs raise IsCredibleError with status (0 for network errors and timeouts) and retryable, and already retry 429 and 5xx with backoff. They also reject malformed input (such as an invalid email address) before sending anything: the JavaScript SDK throws an Error, the Python SDK raises ValueError. Treat those like any other failure: fail open, or validate the input first.

Testing the integration#

  • Use a separate test account or clearly fake identities; checks are remembered and affect later decisions on the same account.
  • A throwaway address such as test@mailinator.com sent to SYS_DISPOSABLE_EMAIL_001 should return DENY. A normal address such as you@yourcompany.com should return APPROVE.
  • Write unit tests that mock the client and cover all three decisions plus a thrown error (fail open).
  • To remove a test identity afterwards, call POST https://api.iscredible.com/v1/credibility/erase (or iscredible.erase(...)) with its email, user ID, device ID, phone or IP.
  • The free plan allows 100 checks a day; avoid calling isCredible in load tests.

Data and privacy#

  • Data is used only to make decisions for the account that sent it.
  • To honour an end-user's deletion request, call the Erase API (client.erase({ email, userId }) in the SDKs).
  • Mention isCredible (fraud and abuse prevention) in the app's privacy policy as a service provider that receives sign-up and usage signals.

Checklist for the assistant#

Before finishing, confirm each item:

  • The key is read from ISCREDIBLE_API_KEY, is in .env.example with no value, and is never sent to a client or logged.
  • Every call runs on the server, at an event (signup, login, trial, form, expensive action), not on page views.
  • Each call has a short timeout and fails open with a log line.
  • DENY, REVIEW and APPROVE each have defined behaviour; end users see neutral messages only.
  • requestId and reasons are logged for non-approved decisions.
  • No passwords, card numbers or government IDs are sent.
  • Tests cover all three decisions and the error path.
  • The developer is told what changed, which env var to set, and how to get a key (sign up at iscredible.com, then open API key in the dashboard).

Prompt to give your assistant#

Copy this into Claude Code, Cursor or another coding assistant:

Integrate isCredible into this app, following https://iscredible.com/ai-integration.md exactly.
Protect: signup, and any public forms. (Edit this list: login, free trial start, AI/metered usage.)
Use the official SDK for our backend language, read the key from ISCREDIBLE_API_KEY, call it only
from server code, fail open on errors, and keep reasons out of user-facing messages.
Show me the plan before editing, then add tests for APPROVE, REVIEW, DENY and the error path.

Reference#