npm package: @redapro/sdk
Description: Official TypeScript SDK for the Redapro Essay Correction API
Latest version: 0.2.0

# @redapro/sdk

Official TypeScript SDK for the Redapro Essay Correction API.

Auto-generated from OpenAPI spec using [@hey-api/openapi-ts](https://heyapi.dev/).

## Installation

```bash
npm install @redapro/sdk

# Using Bun
bun add @redapro/sdk
```

## Quick Start

```typescript
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

```typescript
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)

```typescript
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)

```typescript
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)

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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`:

```typescript
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

```typescript
// 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:

```typescript
// 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

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
import type {
  // Request types
  PostEssaysVunespData,
  PostEssaysFuvestData,

  // Response types
  GetEssaysVunespByIdResponse,
  GetEssaysFuvestByIdResponse,

  // Error types
  PostEssaysVunespError,

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

## Development

```bash
# 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

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

## License

Proprietary - Redapro