Scrawn LogoScrawn Docs
Scrawn LogoScrawn Docs

API Reference

Complete reference for Scrawn SDK and gRPC API

SDK API

Constructor

new Scrawn(config: { apiKey: string; baseURL: string })

Creates a new Scrawn SDK instance.

Parameters:

ParameterTypeRequiredDescription
apiKeystring as `scrn_${string}`YesYour Scrawn API key (must start with scrn_)
baseURLstringYesBackend server URL

Example:

const scrawn = new Scrawn({
  apiKey: process.env.SCRAWN_KEY as `scrn_${string}`,
  baseURL: process.env.SCRAWN_BASE_URL || 'http://localhost:8069',
});

sdkCallEventConsumer

scrawn.sdkCallEventConsumer(payload: EventPayload): Promise<void>

Track billable usage events.

Parameters:

ParameterTypeRequiredDescription
userIdstringYesUnique user identifier
debitAmountnumberYesAmount to debit from user's balance

Returns: Promise<void>

Example:

await scrawn.sdkCallEventConsumer({
  userId: 'user-123',
  debitAmount: 100,
});

Validation:

  • userId must be a non-empty string
  • debitAmount must be a positive number

Throws:

  • Error with detailed validation message on invalid payload
  • Error on gRPC call failure

middlewareEventConsumer

scrawn.middlewareEventConsumer(config: MiddlewareEventConfig): 
  (req: MiddlewareRequest, res: MiddlewareResponse, next: MiddlewareNext) => Promise<void>

Creates Express-compatible middleware that automatically tracks usage for API requests.

Parameters:

ParameterTypeRequiredDescription
extractorPayloadExtractorYesFunction to extract userId and debitAmount from request
whiteliststring[]NoArray of endpoint patterns to track (takes precedence over blacklist)
blackliststring[]NoArray of endpoint patterns to exclude from tracking

Types:

type PayloadExtractor = (req: MiddlewareRequest) => 
  EventPayload | Promise<EventPayload> | null | Promise<null>;

interface MiddlewareEventConfig {
  extractor: PayloadExtractor;
  whitelist?: string[];
  blacklist?: string[];
}

Returns: Express-compatible middleware function (req, res, next) => Promise<void>

Example:

app.use(scrawn.middlewareEventConsumer({
  extractor: (req): EventPayload => ({
    userId: req.headers['x-user-id'] as string,
    debitAmount: req.body?.cost || 1,
  }),
  blacklist: ['/health', '/api/collect-payment'],
}));

Wildcard Patterns:

  • * matches single path segment (e.g., /api/* matches /api/users but not /api/users/123)
  • ** matches multiple segments (e.g., /api/** matches any path starting with /api/)

Error Handling:

  • Errors are automatically caught and logged without blocking requests
  • Failed tracking attempts don't affect the HTTP response

collectPayment

scrawn.collectPayment(userId: string): Promise<string>

Generate payment checkout link for a user to pay their outstanding amount.

Parameters:

ParameterTypeRequiredDescription
userIdstringYesUnique identifier of the user to collect payment from

Returns: Promise<string> - Checkout URL where the user can complete payment

Example:

const checkoutLink = await scrawn.collectPayment('user-123');

// Redirect user to payment page
res.redirect(checkoutLink);

Validation:

  • userId must be a non-empty string

Throws:

  • Error if userId is invalid
  • Error on gRPC call failure

gRPC API

Scrawn backend exposes three gRPC services for direct integration.

EventService

registerEvent

Registers a billable SDK call event.

Request:

message RegisterEventRequest {
  EventType type = 1;
  string userId = 2;
  oneof data {
    SDKCall sdkCall = 3;
  }
}

message SDKCall {
  SDKCallType sdkCallType = 1;
  float debitAmount = 2;
}

enum EventType {
  EVENT_TYPE_UNSPECIFIED = 0;
  SDK_CALL = 1;
}

enum SDKCallType {
  SDKCallType_UNSPECIFIED = 0;
  RAW = 1;
  MIDDLEWARE_CALL = 2;
}

Response:

message RegisterEventResponse {
  string random = 1;
}

Example (TypeScript):

import { createPromiseClient } from '@connectrpc/connect';
import { EventService } from './gen/event/v1/event_connect';
import { EventType, SDKCallType } from './gen/event/v1/event_pb';

const client = createPromiseClient(EventService, transport);

const response = await client.registerEvent({
  type: EventType.SDK_CALL,
  userId: 'user_123',
  data: {
    case: 'sdkCall',
    value: {
      sdkCallType: SDKCallType.RAW,
      debitAmount: 100
    }
  }
});

console.log('Status:', response.random);

Authentication:

  • Requires valid API key in Authorization header as Bearer token
  • API key must start with scrn_

AuthService

createAPIKey

Creates a new API key for authenticated users.

Request:

message CreateAPIKeyRequest {
  string name = 1;
  int64 expiresIn = 2;  // expiration time in seconds from now
}

Response:

message CreateAPIKeyResponse {
  string apiKeyId = 1;
  string apiKey = 2;
  string name = 3;
  string createdAt = 4;
  string expiresAt = 5;
}

Example:

import { createPromiseClient } from '@connectrpc/connect';
import { AuthService } from './gen/auth/v1/auth_connect';

const client = createPromiseClient(AuthService, transport);

const response = await client.createAPIKey({
  name: 'Production Key',
  expiresIn: BigInt(365 * 24 * 60 * 60)  // 1 year in seconds
});

console.log('API Key:', response.apiKey);
console.log('API Key ID:', response.apiKeyId);
console.log('Expires At:', response.expiresAt);

Authentication:

  • Requires valid API key in Authorization header as Bearer token
  • New API key will be generated with scrn_ prefix

Notes:

  • The API key is returned only once during creation
  • Store the API key securely - it cannot be retrieved later
  • The backend stores a hashed version of the API key

PaymentService

Generates a Lemon Squeezy checkout link for a user to pay their outstanding balance.

Request:

message CreateCheckoutLinkRequest {
  string userId = 1;
}

Response:

message CreateCheckoutLinkResponse {
  string checkoutLink = 1;
}

Example:

import { createPromiseClient } from '@connectrpc/connect';
import { PaymentService } from './gen/payment/v1/payment_connect';

const client = createPromiseClient(PaymentService, transport);

const response = await client.createCheckoutLink({
  userId: 'user_123'
});

console.log('Checkout Link:', response.checkoutLink);
// Redirect user to this URL to complete payment

How it works:

  • The backend calculates the user's outstanding balance automatically
  • A custom-priced checkout session is created via Lemon Squeezy
  • The checkout link is valid for immediate use
  • User ID is passed to the payment provider for tracking

Notes:

  • Payment amount is calculated from stored debit events
  • Uses Lemon Squeezy as the payment provider
  • No need to specify amount - it's calculated server-side

Error Handling

SDK Errors

The SDK throws standard JavaScript Error objects with descriptive messages.

Validation Errors:

try {
  await scrawn.sdkCallEventConsumer({
    userId: '',  // Invalid: empty string
    debitAmount: -5,  // Invalid: negative number
  });
} catch (error) {
  console.error(error.message);
  // "Payload validation failed: userId: must be a non-empty string, debitAmount: must be a positive number"
}

gRPC Connection Errors:

try {
  await scrawn.sdkCallEventConsumer({
    userId: 'user-123',
    debitAmount: 100,
  });
} catch (error) {
  console.error('Failed to register event:', error.message);
}

}


### Backend Error Typesuctured errors using gRPC Connect error codes.

#### AuthError

Authentication and authorization errors.

**Error Types:**

| Type | Code | Description | Solution |
|------|------|-------------|----------|
| `MISSING_HEADER` | `UNAUTHENTICATED` | No Authorization header provided | Include `Authorization: Bearer <api_key>` header |
| `INVALID_HEADER_FORMAT` | `UNAUTHENTICATED` | Authorization header format is wrong | Use format: `Bearer scrn_...` |
| `INVALID_API_KEY` | `UNAUTHENTICATED` | API key is invalid or not found | Check API key is correct and exists |
| `EXPIRED_API_KEY` | `UNAUTHENTICATED` | API key has expired | Create a new API key |
| `REVOKED_API_KEY` | `UNAUTHENTICATED` | API key has been revoked | Create a new API key |
| `DATABASE_ERROR` | `INTERNAL` | Failed to verify API key | Contact support |
| `UNKNOWN` | `INTERNAL` | Unknown authentication error | Contact support |

#### EventError

Event registration and processing errors.

**Error Types:**

| Type | Code | Description | Solution |
|------|------|-------------|----------|
| `INVALID_PAYLOAD` | `INVALID_ARGUMENT` | Event payload is invalid | Check payload structure matches schema |
| `UNSUPPORTED_EVENT_TYPE` | `INVALID_ARGUMENT` | Event type is not supported | Use `SDK_CALL` event type |
| `VALIDATION_FAILED` | `INVALID_ARGUMENT` | Zod schema validation failed | Review validation error details |
| `INVALID_USER_ID` | `INVALID_ARGUMENT` | User ID format is invalid | Provide non-empty string for userId |
| `MISSING_DATA` | `INVALID_ARGUMENT` | Required field is missing | Include all required fields |
| `INVALID_DATA_FORMAT` | `INVALID_ARGUMENT` | Data format doesn't match expected | Check field types and formats |
| `SERIALIZATION_ERROR` | `INTERNAL` | Failed to store event | Contact support |
| `UNKNOWN` | `INTERNAL` | Unknown event processing error | Contact support |

#### PaymentError

Payment and checkout link creation errors.

**Error Types:**

| Type | Code | Description | Solution |
|------|------|-------------|----------|
| `INVALID_USER_ID` | `INVALID_ARGUMENT` | User ID is invalid | Provide valid non-empty userId |
| `VALIDATION_FAILED` | `INVALID_ARGUMENT` | Request validation failed | Check request parameters |
| `MISSING_API_KEY` | `FAILED_PRECONDITION` | Lemon Squeezy API key not configured | Configure `LEMON_SQUEEZY_API_KEY` env var |
| `MISSING_STORE_ID` | `FAILED_PRECONDITION` | Lemon Squeezy store ID not configured | Configure `LEMON_SQUEEZY_STORE_ID` env var |
| `MISSING_VARIANT_ID` | `FAILED_PRECONDITION` | Lemon Squeezy variant ID not configured | Configure `LEMON_SQUEEZY_VARIANT_ID` env var |
| `PRICE_CALCULATION_FAILED` | `INTERNAL` | Failed to calculate user's balance | Ensure user has registered events |
| `STORAGE_ADAPTER_FAILED` | `INTERNAL` | Failed to retrieve data from storage | Check database connectivity |
| `LEMON_SQUEEZY_API_ERROR` | `INTERNAL` | Lemon Squeezy API call failed | Check Lemon Squeezy service status |
| `INVALID_CHECKOUT_RESPONSE` | `INTERNAL` | Invalid response from payment provider | Contact support |
| `CHECKOUT_CREATION_FAILED` | `INTERNAL` | Failed to create checkout link | Review error details |
| `CONFIGURATION_ERROR` | `FAILED_PRECONDITION` | Payment system misconfigured | Check environment configuration |
| `UNKNOWN` | `INTERNAL` | Unknown payment error | Contact support |

| `UNKNOWN` | `INTERNAL` | Unknown payment error | Contact support |

### Error Response Format gRPC Connect error format:

```json
{
  "code": "invalid_argument",
  "message": "Event validation failed: userId: must be a non-empty string",
  "details": []
}

Common gRPC Codes:

  • INVALID_ARGUMENT - Invalid request parameters

  • UNAUTHENTICATED - Authentication failed

  • INTERNAL - Internal server error

  • FAILED_PRECONDITION - Server not properly configured

  • FAILED_PRECONDITION - Server not properly configured

TypestPayload

interface EventPayload {
  userId: string;
  debitAmount: number;
}

Validated using Zod schema:

  • userId: Must be a non-empty string
  • debitAmount: Must be a positive number

MiddlewareRequest

interface MiddlewareRequest {
  method?: string;
  url?: string;
  path?: string;
  body?: any;
  headers?: Record<string, string | string[] | undefined>;
  query?: Record<string, any>;
  params?: Record<string, any>;
  [key: string]: any;
}

Generic request object compatible with Express, Fastify, and other Node.js frameworks.

MiddlewareResponse

interface MiddlewareResponse {
  status?: (code: number) => MiddlewareResponse;
  json?: (data: any) => void;
  send?: (data: any) => void;
  [key: string]: any;
}

Generic response object compatible with Express, Fastify, and other Node.js frameworks.

MiddlewareNext

type MiddlewareNext = (error?: any) => void;

PayloadExtractor

type PayloadExtractor = (
  req: MiddlewareRequest
) => EventPayload | Promise<EventPayload> | null | Promise<null>;

Function that extracts userId and debitAmount from a request. Return null to skip tracking.

MiddlewareEventConfig

interface MiddlewareEventConfig {
  extractor: PayloadExtractor;
  whitelist?: string[];
  blacklist?: string[];
}

Configuration for middlewareEventConsumer.

Configuration for middlewareEventConsumer.

Utilitiesh

matchPath(path: string, pattern: string): boolean

Matches a path against a pattern with wildcard support.

Parameters:

ParameterTypeDescription
pathstringThe path to match
patternstringThe pattern with optional wildcards

Wildcard Patterns:

  • * matches single path segment
  • ** matches multiple segments

Example:

import { matchPath } from '@scrawn/core';

matchPath('/api/users', '/api/*'); // true
matchPath('/api/users/123', '/api/*'); // false
matchPath('/api/users/123', '/api/**'); // true
matchPath('/api/v1/users', '/api/v1/*'); // true

matchPath('/api/v1/users', '/api/v1/*'); // true


## Authenticationequests require authentication via Bearer token in the `Authorization` header.

**API Key Format:**
- Must start with `scrn_` prefix
- Example: `scrn_1234567890abcdef`

**Using the SDK:**

The SDK handles authentication automatically using the `apiKey` provided in the constructor:

```typescript
const scrawn = new Scrawn({
  apiKey: process.env.SCRAWN_KEY as `scrn_${string}`,
  baseURL: 'http://localhost:8069',
});

Direct gRPC Calls:

When making direct gRPC calls without the SDK, include the API key in the Authorization header:

Authorization: Bearer scrn_your_api_key

Authentication Flow:

  1. Backend receives request with Bearer token
  2. Auth interceptor validates the API key format
  3. API key hash is verified against database
  4. API key expiration is checked
  5. API key ID is added to request context for downstream handlers

Security Notes:

  • API keys are hashed using SHA-256 before storage

  • Original API keys are never stored in the database

  • API keys are cached in memory for performance (with TTL)

  • Expired API keys are automatically rejected

  • Expired API keys are automatically rejected

Next Steps