@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/sdkQuick 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 testVersion
import { VERSION } from "@redapro/sdk";
console.log("SDK Version:", VERSION); // "0.2.0"License
Proprietary - Redapro
