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:
| Parameter | Type | Required | Description |
|---|---|---|---|
apiKey | string as `scrn_${string}` | Yes | Your Scrawn API key (must start with scrn_) |
baseURL | string | Yes | Backend 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | Unique user identifier |
debitAmount | number | Yes | Amount to debit from user's balance |
Returns: Promise<void>
Example:
await scrawn.sdkCallEventConsumer({
userId: 'user-123',
debitAmount: 100,
});Validation:
userIdmust be a non-empty stringdebitAmountmust be a positive number
Throws:
Errorwith detailed validation message on invalid payloadErroron 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
extractor | PayloadExtractor | Yes | Function to extract userId and debitAmount from request |
whitelist | string[] | No | Array of endpoint patterns to track (takes precedence over blacklist) |
blacklist | string[] | No | Array 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/usersbut 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | Unique 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:
userIdmust be a non-empty string
Throws:
ErrorifuserIdis invalidErroron 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
Authorizationheader 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
Authorizationheader 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
createCheckoutLink
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 paymentHow 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 stringdebitAmount: 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): booleanMatches a path against a pattern with wildcard support.
Parameters:
| Parameter | Type | Description |
|---|---|---|
path | string | The path to match |
pattern | string | The 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/*'); // truematchPath('/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_keyAuthentication Flow:
- Backend receives request with Bearer token
- Auth interceptor validates the API key format
- API key hash is verified against database
- API key expiration is checked
- 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