npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@redapro/sdk

v0.2.0

Published

Official TypeScript SDK for the Redapro Essay Correction API

Readme

@redapro/sdk

Official TypeScript SDK for the Redapro Essay Correction API.

Auto-generated from OpenAPI spec using @hey-api/openapi-ts.

Installation

npm install @redapro/sdk

# Using Bun
bun add @redapro/sdk

Quick Start

import { createRedaproClient, postEssaysVunesp, getEssaysVunespById } from "@redapro/sdk";

// Create client instance
const client = createRedaproClient({
  baseUrl: "https://api.redapro.com",
  apiKey: "rp_live_xxxxxxxxxxxx",
});

// Submit a VUNESP essay
const { data, error } = await postEssaysVunesp({
  client,
  body: {
    theme: "A importância da educação ambiental nas escolas",
    content: "Texto da redação com pelo menos 200 caracteres...",
    studentName: "João Silva",
  },
});

if (error) {
  console.error("Error:", error);
} else {
  console.log("Essay submitted:", data.essayId);
  console.log("Job ID:", data.jobId);
}

Features

  • Type-safe: Full TypeScript support with auto-generated types
  • Retry logic: Automatic retry with exponential backoff for transient errors
  • Timeout handling: Configurable request timeouts
  • Webhook verification: Server-side utilities for webhook signature verification

Client Configuration

import { createRedaproClient } from "@redapro/sdk";

const client = createRedaproClient({
  // Required
  baseUrl: "https://api.redapro.com",
  apiKey: "rp_live_xxxxxxxxxxxx",

  // Optional
  timeout: 30000, // Request timeout in ms (default: 30000)
  retry: {
    attempts: 3,           // Max retry attempts (default: 3)
    delay: 1000,           // Initial delay in ms (default: 1000)
    statusCodes: [429, 500, 502, 503, 504], // Status codes to retry
  },
  onError: (error) => {
    console.error("Request failed:", error);
  },
});

API Reference

Essay Correction Endpoints

VUNESP Essays (Score: 0-28)

import {
  postEssaysVunesp,       // Submit text essay
  postEssaysVunespUpload, // Upload image essay
  getEssaysVunespById,    // Get correction result
} from "@redapro/sdk";

// Submit text-based essay
const { data } = await postEssaysVunesp({
  client,
  body: {
    theme: "Tema da redação",
    content: "Texto da redação (mínimo 200 caracteres)...",
    title: "Título (opcional para VUNESP)",
    studentName: "Nome do aluno",
    studentId: "ID do aluno",
    metadata: { classroom: "3A" },
  },
});

// Upload image-based essay
const { data } = await postEssaysVunespUpload({
  client,
  body: {
    theme: "Tema da redação",
    image: file, // Blob | File (JPEG/PNG, max 10MB)
    title: "Título",
    studentName: "Nome do aluno",
  },
});

// Get correction result
const { data } = await getEssaysVunespById({
  client,
  path: { id: "essay-uuid" },
});

// Response when completed
if (data.status === "completed") {
  console.log("Score:", data.correction.score); // 0-28
  console.log("Grades:", data.correction.grades);
  // { tema: 0-7, estrutura: 0-7, linguagem: 0-7, coesao: 0-7 }
}

FUVEST Essays (Score: 0-50)

import {
  postEssaysFuvest,       // Submit text essay
  postEssaysFuvestUpload, // Upload image essay
  getEssaysFuvestById,    // Get correction result
} from "@redapro/sdk";

// Submit text-based essay (title is REQUIRED for FUVEST)
const { data } = await postEssaysFuvest({
  client,
  body: {
    theme: "Tema da redação",
    title: "Título (OBRIGATÓRIO para FUVEST)",
    content: "Texto da redação (mínimo 200 caracteres)...",
    studentName: "Nome do aluno",
  },
});

// Get correction result
const { data } = await getEssaysFuvestById({
  client,
  path: { id: "essay-uuid" },
});

// Response when completed
if (data.status === "completed") {
  console.log("Score:", data.correction.score); // 0-50
  console.log("Criteria:", data.correction.criteria);
  // { desenvolvimentoTema: 0-3, compreensaoGenero: 0-3, recursosLinguisticos: 0-3, normaPadrao: 0-3 }
  console.log("Weighted Scores:", data.correction.weightedScores);
  // { desenvolvimentoTema: 0-9, compreensaoGenero: 0-6, recursosLinguisticos: 0-6, normaPadrao: 0-9 }
  console.log("Title detected:", data.correction.titlePresent);
}

ENEM Essays (Score: 0-1000)

import {
  postEssaysEnem,       // Submit text essay
  postEssaysEnemUpload, // Upload image essay
  getEssaysEnemById,    // Get correction result
} from "@redapro/sdk";

// Submit text-based essay
const { data } = await postEssaysEnem({
  client,
  body: {
    theme: "A persistência da violência contra a mulher na sociedade brasileira",
    content: "Texto da redação (mínimo 200 caracteres)...",
    studentName: "Nome do aluno",
    studentId: "ID do aluno",
    metadata: { classroom: "3A" },
  },
});

// Get correction result
const { data } = await getEssaysEnemById({
  client,
  path: { id: "essay-uuid" },
});

// Response when completed
if (data.status === "completed") {
  console.log("Score:", data.correction.score); // 0-1000
  console.log("Performance:", data.correction.performanceLevel);
  // "excelente" (900+) | "muitoBom" (800-880) | "bom" (700-780) | "mediano" (≤680)
  console.log("Grades:", data.correction.grades);
  // { C1: 0-200, C2: 0-200, C3: 0-200, C4: 0-200, C5: 0-200 }
  console.log("Competency Analysis:", data.correction.competencyAnalysis);
  // { C1: "análise...", C2: "...", C3: "...", C4: "...", C5: "..." }
  console.log("Study Plan:", data.correction.studyPlan);
  // { C1: ["dica1", ...], C2: [...], ... }
  console.log("Next Essay Guidance:", data.correction.nextEssayGuidance);
}

Generic Essay Operations

import { getEssays, getEssaysById, getEssaysByIdImage } from "@redapro/sdk";

// List all essays with pagination
const { data } = await getEssays({
  client,
  query: {
    limit: 20,
    status: "completed",
    universityCode: "VUNESP",
    cursor: "last-essay-id",
  },
});

// Get any essay by ID (any university)
const { data } = await getEssaysById({
  client,
  path: { id: "essay-uuid" },
});

// Get presigned URL for essay image
const { data } = await getEssaysByIdImage({
  client,
  path: { id: "essay-uuid" },
});
console.log("Download URL:", data.url);

Credits Management

import { getCredits, getCreditsHistory } from "@redapro/sdk";

// Get current balance
const { data } = await getCredits({ client });
console.log("Balance:", data.balance);
console.log("Unlimited:", data.isUnlimited);

// Get transaction history
const { data } = await getCreditsHistory({
  client,
  query: { limit: 50, type: "usage" },
});

Webhooks Management

import {
  getWebhooks,
  postWebhooks,
  patchWebhooksById,
  deleteWebhooksById,
  getWebhooksByIdEvents,
} from "@redapro/sdk";

// Create webhook
const { data } = await postWebhooks({
  client,
  body: {
    url: "https://your-app.com/webhooks/redapro",
    events: ["essay.completed", "essay.failed"],
    secret: "optional-custom-secret", // Auto-generated if not provided
  },
});
console.log("Webhook ID:", data.id);
console.log("Secret:", data.secret); // Save this for verification!

// List webhooks
const { data } = await getWebhooks({ client });

// Update webhook
await patchWebhooksById({
  client,
  path: { id: "webhook-id" },
  body: { isActive: false },
});

// Get delivery history
const { data } = await getWebhooksByIdEvents({
  client,
  path: { id: "webhook-id" },
});

Universities Info

import { getUniversities, getUniversitiesByCode } from "@redapro/sdk";

// List all supported universities
const { data } = await getUniversities({ client });
// [{ code: "ENEM", name: "ENEM", maxScore: 1000 }, ...]

// Get specific university details
const { data } = await getUniversitiesByCode({
  client,
  path: { code: "VUNESP" },
});

Server-Side Utilities

For webhook signature verification, import from @redapro/sdk/server:

import { verifyWebhookSignature, WEBHOOK_EVENTS } from "@redapro/sdk/server";

// Express/Hono/Fastify handler
app.post("/webhooks/redapro", async (req, res) => {
  const signature = req.headers["x-redapro-signature"];
  const payload = JSON.stringify(req.body);

  const result = verifyWebhookSignature(
    payload,
    signature,
    process.env.REDAPRO_WEBHOOK_SECRET!
  );

  if (!result.valid) {
    return res.status(401).json({ error: result.error });
  }

  const { event } = result;

  switch (event.event) {
    case WEBHOOK_EVENTS.ESSAY_COMPLETED:
      console.log("Essay completed:", event.essayId);
      // Process completed essay...
      break;
    case WEBHOOK_EVENTS.ESSAY_FAILED:
      console.log("Essay failed:", event.essayId);
      // Handle failure...
      break;
  }

  return res.status(200).json({ received: true });
});

Response Types

Correction Result Structure

// ENEM Correction (status: "completed")
type EnemCorrection = {
  score: number;           // 0-1000 (sum of C1-C5)
  performanceLevel: "excelente" | "muitoBom" | "bom" | "mediano";
  grades: {
    C1: number;            // 0-200 (Norma culta)
    C2: number;            // 0-200 (Tema)
    C3: number;            // 0-200 (Argumentação)
    C4: number;            // 0-200 (Coesão)
    C5: number;            // 0-200 (Proposta de intervenção)
  };
  competencyAnalysis: {    // Detailed analysis per competency
    C1: string;
    C2: string;
    C3: string;
    C4: string;
    C5: string;
  };
  studyPlan: {             // Study tips for competencies below 200
    C1: string[];
    C2: string[];
    C3: string[];
    C4: string[];
    C5: string[];
  };
  rewriteSuggestions: Array<{
    original: string;
    suggestion: string;
    justification: string;
  }>;
  nextEssayGuidance: string; // Guidance for next essay
  marks: Mark[];
  feedback: string;
  strengths: string[];
  improvements: string[];
  statistics: object | null;
  extractedText: string | null;
};

// VUNESP Correction (status: "completed")
type VunespCorrection = {
  score: number;           // 0-28 (sum of grades)
  grades: {
    tema: number;          // 0-7
    estrutura: number;     // 0-7
    linguagem: number;     // 0-7
    coesao: number;        // 0-7
  };
  marks: Mark[];           // Text marks and suggestions
  feedback: string;        // General feedback
  strengths: string[];     // Identified strengths
  improvements: string[];  // Suggested improvements
  statistics: object | null;
  extractedText: string | null; // Only for image uploads
};

// FUVEST Correction (status: "completed")
type FuvestCorrection = {
  score: number;           // 0-50 (scaled from weighted scores)
  criteria: {
    desenvolvimentoTema: number;    // 0-3
    compreensaoGenero: number;      // 0-3
    recursosLinguisticos: number;   // 0-3
    normaPadrao: number;            // 0-3
  };
  weightedScores: {
    desenvolvimentoTema: number;    // 0-9 (×3)
    compreensaoGenero: number;      // 0-6 (×2)
    recursosLinguisticos: number;   // 0-6 (×2)
    normaPadrao: number;            // 0-9 (×3)
  };
  titlePresent: boolean;   // FUVEST requires title
  marks: Mark[];
  feedback: string;
  strengths: string[];
  improvements: string[];
  statistics: object | null;
  extractedText: string | null;
};

// Mark structure (shared across all universities)
type Mark = {
  type: "ORTOGRAFIA" | "GRAMÁTICA" | "CLAREZA" | "OUTROS"
      | "V_TEMA" | "V_ESTRUTURA" | "V_LINGUAGEM" | "V_COESAO"  // VUNESP
      | "F_TEMA" | "F_GENERO" | "F_RECURSOS" | "F_NORMA"       // FUVEST
      | "C1" | "C2" | "C3" | "C4" | "C5";                       // ENEM
  id: string;              // e.g., "SPELLING_ERROR", "SHORT_SENTENCE"
  origin: "custom" | "llm" | "rule";
  title: string;           // Display title
  message: string;         // Detailed explanation
  replacements: string[];  // Suggested corrections

  // Text matching (preferred) - v0.2.0+
  matchText: string;       // Exact text from essay to highlight
  contextBefore?: string;  // Context before for disambiguation
  contextAfter?: string;   // Context after for disambiguation

  // Legacy positioning (deprecated)
  positions?: { start: number; length: number }[];

  active?: boolean;
  advanced?: boolean;
  level: "word" | "sentence" | "paragraph";
};

Text Highlighting with Marks (v0.2.0+)

Marks now use matchText for reliable text highlighting instead of character positions:

// Find mark positions in essay text
function findMarkPositions(essayText: string, mark: Mark) {
  const { matchText, contextBefore, contextAfter } = mark;

  // Find all occurrences of matchText
  const positions: { start: number; length: number }[] = [];
  let searchStart = 0;

  while (true) {
    const index = essayText.indexOf(matchText, searchStart);
    if (index === -1) break;

    // Use context to disambiguate if provided
    let isMatch = true;

    if (contextBefore) {
      const before = essayText.slice(Math.max(0, index - 50), index);
      if (!before.includes(contextBefore)) isMatch = false;
    }

    if (contextAfter && isMatch) {
      const after = essayText.slice(index + matchText.length, index + matchText.length + 50);
      if (!after.includes(contextAfter)) isMatch = false;
    }

    if (isMatch) {
      positions.push({ start: index, length: matchText.length });
    }

    searchStart = index + 1;
  }

  return positions;
}

// Example: Highlight marks in React
function HighlightedEssay({ essayText, marks }: { essayText: string; marks: Mark[] }) {
  // Build segments with highlights
  const segments = useMemo(() => {
    const allPositions = marks.flatMap(mark =>
      findMarkPositions(essayText, mark).map(pos => ({ ...pos, mark }))
    ).sort((a, b) => a.start - b.start);

    // ... render logic
  }, [essayText, marks]);

  return <div>{segments}</div>;
}

Status Flow

pending → processing → completed
                    ↘ failed
  • pending: Essay queued for processing
  • processing: Currently being corrected by AI
  • completed: Correction available
  • failed: Processing failed (check error field)

Error Handling

const { data, error } = await postEssaysVunesp({
  client,
  body: { theme: "...", content: "..." },
});

if (error) {
  switch (error.status) {
    case 400:
      console.error("Validation error:", error.message);
      break;
    case 402:
      console.error("Insufficient credits");
      // error.code === "INSUFFICIENT_CREDITS"
      break;
    case 401:
      console.error("Invalid API key");
      break;
    case 404:
      console.error("Not found");
      break;
    case 500:
      console.error("Server error:", error.message);
      break;
  }
}

Test Mode

Use test API keys (prefix rp_test_) for testing without consuming credits:

const client = createRedaproClient({
  baseUrl: "https://api.redapro.com",
  apiKey: "rp_test_xxxxxxxxxxxx", // Test key - no credits consumed
});

Note: Test keys follow the Stripe pattern: rp_live_ for production, rp_test_ for testing.

Polling Pattern

Since corrections are async, use polling to wait for results:

async function waitForCorrection(
  client: Client,
  essayId: string,
  maxAttempts = 30,
  intervalMs = 2000
): Promise<GetEssaysVunespByIdResponse | null> {
  for (let i = 0; i < maxAttempts; i++) {
    const { data } = await getEssaysVunespById({
      client,
      path: { id: essayId },
    });

    if (data.status === "completed" || data.status === "failed") {
      return data;
    }

    await new Promise((r) => setTimeout(r, intervalMs));
  }
  return null;
}

// Usage
const { data: submission } = await postEssaysVunesp({ client, body: {...} });
const result = await waitForCorrection(client, submission.essayId);

TypeScript Types

All types are auto-generated and exported:

import type {
  // Request types
  PostEssaysVunespData,
  PostEssaysFuvestData,

  // Response types
  GetEssaysVunespByIdResponse,
  GetEssaysFuvestByIdResponse,

  // Error types
  PostEssaysVunespError,

  // Common types
  GetCreditsResponse,
  GetWebhooksResponse,
} from "@redapro/sdk";

Development

# Generate SDK from OpenAPI spec (requires API running)
bun run generate

# Build
bun run build

# Type check
bun run check-types

# Run tests
bun test

Version

import { VERSION } from "@redapro/sdk";
console.log("SDK Version:", VERSION); // "0.2.0"

License

Proprietary - Redapro