DEV Community

Mohab Abdelkarim
Mohab Abdelkarim

Posted on Edited on

How to Add AI Content Detection to Your App With an API (Developer Guide)

If your product accepts user-submitted text, an essay, a cover letter, a marketplace listing, a forum post, you're already dealing with AI-generated content whether or not you've built anything to handle it. The question isn't whether to add detection. It's how to wire it in without turning it into a multi-week project.

This guide walks through it end to end using Walter's AI Detector API: getting a scoped key, making your first request, parsing the response correctly, using the sentence-level breakdown most integrations ignore, and handling false positives properly.

Table of Contents

  1. What You're Building
  2. Getting a Scoped API Key
  3. Making Your First Request
  4. Understanding the Response
  5. The Sentence-Level Array Is the Point
  6. Sync vs Async
  7. Wiring It Into Your App
  8. Handling False Positives Properly
  9. Common Integration Patterns
  10. Credits and Cost Control
  11. FAQs

1. What You're Building

By the end of this you'll have a function that takes a string, sends it to the detector, and returns a calibrated probability plus a per-sentence breakdown you can act on: flag, route for review, log, or display.

The API scores text from GPT, Claude, Gemini, and other major models. It returns a float, not a boolean verdict, which matters more than it sounds once you reach the false-positives section.

Base URL for everything below:

https://developer-portal.walterwrites.ai/api
Enter fullscreen mode Exit fullscreen mode

2. Getting a Scoped API Key

Sign in to the Developer Portal at platform.walterwrites.ai/api-keys and create a key. You select scopes at creation: Humanizer, AI Detector, Image Detector, or any combination. The detector endpoint specifically requires the ai_detector scope.

Two things to get right immediately.

Scope narrowly. If a service only detects, don't grant it humanizer scope. Costs nothing, standard least privilege.

The full key is shown once. Copy it at creation into your secrets manager. There's no retrieval later.

Keep the key server-side. This is a backend integration, same reasoning as any other secret you wouldn't ship in a browser bundle.

3. Making Your First Request

A single POST:

curl -X POST https://developer-portal.walterwrites.ai/api/detector/ \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "content": "Artificial intelligence has revolutionized numerous industries by automating complex tasks and enhancing decision-making processes."
  }'
Enter fullscreen mode Exit fullscreen mode

Two details that trip people up when adapting a wrapper from another API. Auth is a custom X-API-Key header, not Authorization: Bearer. And the text field is content, not text or input.

Optional parameters: callback_url, poll, and webhook_secret, all covered in section 6.

4. Understanding the Response

{
  "status": "success",
  "task_id": null,
  "result": "ai",
  "ai_score": 0.87,
  "items": [
    {
      "text": "Artificial intelligence has revolutionized numerous industries by automating complex tasks and enhancing decision-making processes.",
      "prediction": "ai-generated",
      "ai_score": 0.87
    }
  ],
  "service_name": "detector_service",
  "execution_time": 1.52,
  "word_count": 18,
  "credits_remaining": 1982,
  "message": "Text has been successfully checked",
  "user": { "id": 12345 }
}
Enter fullscreen mode Exit fullscreen mode

ai_score is a float from 0.0 to 1.0 and is what you build most logic around. result is a convenience binary that returns "ai" when ai_score > 0.5 and "human" otherwise.

Resist collapsing ai_score into result in your own storage layer. A 0.52 and a 0.97 both return "ai", but they're not the same signal, and you'll want that distinction the first time someone disputes a flag.

The documented score bands are worth encoding as constants rather than inventing your own thresholds blind:

  • 0.0 to 0.3: Human, high confidence
  • 0.3 to 0.5: Likely human, medium confidence
  • 0.5 to 0.7: Likely AI, medium confidence
  • 0.7 to 1.0: AI-generated, high confidence

Note the two medium-confidence bands straddling 0.5. That's where your review queue should be pointed.

credits_remaining is the field most integrations ignore and then regret ignoring. Log it every call and you get quota monitoring for free.

5. The Sentence-Level Array Is the Point

The items array is the most useful thing in the response and the most commonly wasted.

Each entry carries the sentence text, a prediction of "ai-generated" or "original", and its own ai_score. That turns an opaque document percentage into something a human reviewer can actually evaluate.

Consider a 2,000-word submission scoring 0.71 overall. That could be uniformly borderline text, or it could be two heavily-flagged paragraphs dragging up an otherwise clean document. Those demand completely different responses, and the document score alone can't tell them apart.

Store the whole array, not just the top-level number:

const flaggedSentences = data.items.filter(
  (item) => item.prediction === "ai-generated" && item.ai_score > 0.7
);

console.log(
  `${flaggedSentences.length} of ${data.items.length} sentences flagged`
);
Enter fullscreen mode Exit fullscreen mode

When a user disputes a flag six weeks later, the document score tells you nothing and the sentence array tells you everything. It's also what lets you build a highlight-in-context review UI instead of showing a reviewer a bare percentage and asking them to trust it.

6. Sync vs Async

The endpoint supports four processing modes, selected by which optional parameters you send:

  • No optional params: sync, 200 OK, result in the response body
  • callback_url: async, 202 Accepted, result POSTed to your URL
  • poll: true: async, 202 Accepted, fetch via GET /api/jobs/{task_id}/
  • callback_url + poll: true: async, delivered both ways

Async requests return task_id populated and result, ai_score, and items all null. Don't parse those fields before checking status.

curl -X POST https://developer-portal.walterwrites.ai/api/detector/ \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "content": "Your long text here...",
    "poll": true,
    "callback_url": "https://your-server.com/webhook",
    "webhook_secret": "your_secret_123"
  }'
Enter fullscreen mode Exit fullscreen mode

Two operational notes on webhooks. Your webhook_secret comes back in the X-Webhook-Secret header so you can verify the request actually originated from Walter, and you should check it. And callback_url must be publicly routable, since requests to private, internal, or reserved IPs are blocked. That last one bites during local development, so use a tunnel or enable polling instead.

Enabling both webhook and polling on the same request is a reasonable belt-and-braces default if webhook delivery reliability is a concern in your infrastructure.

7. Wiring It Into Your App

A Node wrapper handling timeouts and rate limits as expected conditions rather than surprises:

const BASE = "https://developer-portal.walterwrites.ai/api";

async function detectAiContent(content, { callbackUrl, timeoutMs = 15000 } = {}) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  const body = { content };
  if (callbackUrl) body.callback_url = callbackUrl;

  try {
    const res = await fetch(`${BASE}/detector/`, {
      method: "POST",
      headers: {
        "X-API-Key": process.env.WALTER_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
      signal: controller.signal,
    });

    if (res.status === 429) {
      const retryAfter = Number(res.headers.get("retry-after") || 5);
      throw Object.assign(new Error("rate_limited"), { retryAfter });
    }
    if (!res.ok) throw new Error(`Detector returned ${res.status}`);

    const data = await res.json();

    // Async request: nothing to score yet
    if (data.status === "pending") {
      return { pending: true, taskId: data.task_id };
    }

    return {
      pending: false,
      score: data.ai_score,
      verdict: data.result,
      sentences: data.items,
      wordCount: data.word_count,
      creditsRemaining: data.credits_remaining,
    };
  } catch (err) {
    if (err.name === "AbortError") throw new Error("detector_timeout");
    throw err;
  } finally {
    clearTimeout(timer);
  }
}
Enter fullscreen mode Exit fullscreen mode

And the Python equivalent, with backoff on rate limits:

import os
import time
import requests

BASE = "https://developer-portal.walterwrites.ai/api"
HEADERS = {
    "X-API-Key": os.environ["WALTER_API_KEY"],
    "Content-Type": "application/json",
}

def detect_ai_content(content: str, max_retries: int = 3) -> dict:
    for attempt in range(max_retries):
        r = requests.post(f"{BASE}/detector/", headers=HEADERS,
                          json={"content": content}, timeout=15)

        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
            continue

        r.raise_for_status()
        data = r.json()

        if data["status"] == "pending":
            return {"pending": True, "task_id": data["task_id"]}

        return {
            "pending": False,
            "score": data["ai_score"],
            "verdict": data["result"],
            "sentences": data["items"],
            "word_count": data["word_count"],
            "credits_remaining": data["credits_remaining"],
        }

    raise RuntimeError("detector: retries exhausted")
Enter fullscreen mode Exit fullscreen mode

Both fail open by design. If this call sits inside a submission flow, a detector outage must never become a submission outage.

8. Handling False Positives Properly

This is where detection integrations go wrong, and it's the difference between a useful feature and a support ticket generator.

Walter's own published figures put false positive rates at roughly 4 to 12 percent at a 0.5 confidence threshold, meaning legitimate human writing gets incorrectly flagged somewhere between one in 25 and one in 8 samples. That isn't a bug to route around, it's the actual behavior of probabilistic detection, and it concentrates predictably: non-native English writing, highly formal or technical prose, anything under 100 words, and heavily edited human content all reduce the natural variation detectors read as a human signal.

The response is a tiered review workflow, not a single hard cutoff:

function triage(result) {
  if (result.pending) return "queued";
  if (result.score < 0.3) return "clear";
  if (result.score < 0.7) return "needs_review";
  return "high_confidence_flag";
}
Enter fullscreen mode Exit fullscreen mode

Those boundaries map directly onto the documented confidence bands rather than numbers someone picked arbitrarily. Three buckets give reviewers somewhere to apply judgment on the middle tier, instead of a system that either does nothing or overreacts.

If you're building for a high-stakes context like academic integrity or hiring, build the appeal path before you launch, not after the first complaint. And set a minimum word count before scoring at all, since short text carries too little signal to be meaningful.

It's also worth knowing this isn't permanently solved. Sadasivan et al. 2023, "Can AI-Generated Text Be Reliably Detected?", argued that detection evasion is theoretically unbounded as models improve, which is why detectors get retrained against newer model output rather than shipped once as a static classifier. Build assuming today's accuracy isn't a permanent guarantee.

9. Common Integration Patterns

LMS and edtech. Score at upload, surface a per-student dashboard, route flags into a review queue rather than an automatic penalty. The sentence-level array is what makes an instructor-facing view defensible.

Content marketplaces and publishers. Flag at ingestion before content goes live, so low-effort AI submissions never reach an editor's queue.

HR and recruiting. Cover letters, take-homes, written interview responses. This is where tiered review matters most, since a wrongly flagged candidate is a real cost, not a UX annoyance.

10. Credits and Cost Control

Billing is credit-based and easy to reason about: 1 credit = 1 word for most text endpoints. Plagiarism scanning is a flat 2 credits per scan and image detection is priced by resolution, if you add those later.

Every endpoint draws from one shared monthly balance, so a detect-then-humanize pipeline doesn't require forecasting two budgets. Published tiers start at $49/month for 300,000 words and scale to $1,699/month for 25 million, with a free tier requiring no card to build against first.

Three levers that actually control spend: set a minimum length before scoring, cache by input hash so identical submissions aren't billed twice, and log credits_remaining so you can alert on depletion before a pipeline starts failing.

11. FAQs

What authentication does the detector API use?
A custom X-API-Key header, not bearer tokens. The key needs ai_detector scope, selected when you create it in the Developer Portal.

What's the request field called?
content. Optional parameters are callback_url, poll, and webhook_secret.

Do I need async processing for single checks?
No. Omit the optional parameters and you get a synchronous 200 OK with the result inline. Async is for long documents and batch work.

How do I read the AI score?
It's a float from 0.0 to 1.0, with documented bands: under 0.3 human, 0.3 to 0.5 likely human, 0.5 to 0.7 likely AI, above 0.7 AI-generated. The binary result field returns "ai" above 0.5.

Does it give sentence-level results?
Yes, in the items array, each with its own text, prediction, and score. Store the array rather than just the document score, since it's what makes a flag reviewable later.

What false positive rate should I plan for?
Walter publishes roughly 4 to 12 percent at a 0.5 threshold. Build your review workflow around that expectation rather than treating a flag as automatically correct.

Should a score alone drive an automated decision?
Not for anything with real consequences. Use a calibrated threshold that routes borderline and high-confidence flags to a human, with an appeal path in place before launch.

Why is my webhook not firing in local development?
callback_url must be publicly routable. Requests to private, internal, or reserved IPs are blocked, so use a tunnel or switch to poll: true locally.

Top comments (0)