Scrawn LogoScrawn Docs
Scrawn LogoScrawn Docs

API Reference

Complete reference for Scrawn SDK and gRPC API

SDK API

New : Tags for pricing. Use debitTag for backend price lookup instead of passing a raw amount.

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
debitAmountnumberNoAmount to debit from user's balance
debitTagstringNoPrice tag configured in your backend

Returns: Promise<void>

Example:

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

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

Validation:

  • userId must be a non-empty string
  • Either debitAmount or debitTag must be provided
  • debitAmount must be a positive number
  • debitTag must be a non-empty string

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 debit info 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;

  oneof debit {
    float amount = 2;
    string tag = 3;
  }
}

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,
      debit: {
        case: 'amount',
        value: 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:

TypeCodeDescriptionSolution
MISSING_HEADERUNAUTHENTICATEDNo Authorization header providedInclude Authorization: Bearer <api_key> header
INVALID_HEADER_FORMATUNAUTHENTICATEDAuthorization header format is wrongUse format: Bearer scrn_...
INVALID_API_KEYUNAUTHENTICATEDAPI key is invalid or not foundCheck API key is correct and exists
EXPIRED_API_KEYUNAUTHENTICATEDAPI key has expiredCreate a new API key
REVOKED_API_KEYUNAUTHENTICATEDAPI key has been revokedCreate a new API key
DATABASE_ERRORINTERNALFailed to verify API keyContact support
UNKNOWNINTERNALUnknown authentication errorContact support

EventError

Event registration and processing errors.

Error Types:

TypeCodeDescriptionSolution
INVALID_PAYLOADINVALID_ARGUMENTEvent payload is invalidCheck payload structure matches schema
UNSUPPORTED_EVENT_TYPEINVALID_ARGUMENTEvent type is not supportedUse SDK_CALL event type
VALIDATION_FAILEDINVALID_ARGUMENTZod schema validation failedReview validation error details
INVALID_USER_IDINVALID_ARGUMENTUser ID format is invalidProvide non-empty string for userId
MISSING_DATAINVALID_ARGUMENTRequired field is missingInclude all required fields
INVALID_DATA_FORMATINVALID_ARGUMENTData format doesn't match expectedCheck field types and formats
SERIALIZATION_ERRORINTERNALFailed to store eventContact support
UNKNOWNINTERNALUnknown event processing errorContact support

PaymentError

Payment and checkout link creation errors.

Error Types:

TypeCodeDescriptionSolution
INVALID_USER_IDINVALID_ARGUMENTUser ID is invalidProvide valid non-empty userId
VALIDATION_FAILEDINVALID_ARGUMENTRequest validation failedCheck request parameters
MISSING_API_KEYFAILED_PRECONDITIONLemon Squeezy API key not configuredConfigure LEMON_SQUEEZY_API_KEY env var
MISSING_STORE_IDFAILED_PRECONDITIONLemon Squeezy store ID not configuredConfigure LEMON_SQUEEZY_STORE_ID env var
MISSING_VARIANT_IDFAILED_PRECONDITIONLemon Squeezy variant ID not configuredConfigure LEMON_SQUEEZY_VARIANT_ID env var
PRICE_CALCULATION_FAILEDINTERNALFailed to calculate user's balanceEnsure user has registered events
STORAGE_ADAPTER_FAILEDINTERNALFailed to retrieve data from storageCheck database connectivity
LEMON_SQUEEZY_API_ERRORINTERNALLemon Squeezy API call failedCheck Lemon Squeezy service status
INVALID_CHECKOUT_RESPONSEINTERNALInvalid response from payment providerContact support
CHECKOUT_CREATION_FAILEDINTERNALFailed to create checkout linkReview error details
CONFIGURATION_ERRORFAILED_PRECONDITIONPayment system misconfiguredCheck environment configuration
UNKNOWNINTERNALUnknown payment errorContact support

Error Response Format

All backend errors follow the gRPC Connect error format:

{
  "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

Types

EventPayload

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

Validated using Zod schema:

  • userId: Must be a non-empty string
  • Exactly one of debitAmount or debitTag must be provided
  • debitAmount: Must be a positive number
  • debitTag: Must be a non-empty string

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 either debitAmount or debitTag from a request. Return null to skip tracking.

MiddlewareEventConfig

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

Configuration for middlewareEventConsumer.

Utilities

matchPath

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

Authentication

All API requests 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:

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

Next Steps