Jobcelis

Official Dart/Flutter SDK for the Jobcelis Event Infrastructure Platform.

All API calls go to https://jobcelis.com by default -- you only need your API key to get started.

Installation

# pubspec.yaml
dependencies:
  jobcelis: ^1.0.0

Then run:

dart pub get
# or for Flutter:
flutter pub get

Requirements

  • Dart 3.0+ / Flutter 3.10+

Quick Start

import 'package:jobcelis/jobcelis.dart';

// Only your API key is required -- connects to https://jobcelis.com automatically
final client = JobcelisClient(apiKey: 'your_api_key');
final event = await client.sendEvent('order.created', {'order_id': '123'});
print(event);

// Don't forget to close when done
client.close();

Custom URL: If you're self-hosting Jobcelis, you can override the base URL:

final client = JobcelisClient(apiKey: 'your_api_key', baseURL: 'https://your-instance.example.com');

Retries

The client retries failed requests automatically, so a short outage (such as a deploy) does not lose your events:

  • Network errors (connection refused or reset, DNS, TLS, timeouts) and HTTP 429, 502, 503 and 504 are retried.
  • GET, PUT, PATCH and DELETE are retried on all of them. POST is retried on 429/502/503/504, and on network errors only when it carries an idempotency key.
  • Up to 6 retries by default with exponential backoff (1 s, 2 s, 4 s, 8 s, 16 s, 30 s, ±20 % jitter, about 60 s in total). A Retry-After header is honoured (up to 60 s).
  • sendEvent() sends an X-Idempotency-Key header (a random UUID unless you pass idempotencyKey:), and sendEvents() adds an idempotency_key to every event that has none, so a retry never creates a duplicate event.
// Change the number of retries, or pass 0 to disable them
final client = JobcelisClient(apiKey: 'your_api_key', maxRetries: 3);

// Use your own idempotency key
await client.sendEvent('order.created', {'order_id': '123'}, idempotencyKey: 'order-123-created');

After the last retry the client throws the same JobcelisException (or HTTP client exception) it would throw without retries.

Authentication

The auth methods do not require an API key. Use them to register, log in, and manage JWT tokens.

final client = JobcelisClient(apiKey: '');

// Register a new account
final user = await client.register('alice@example.com', 'SecurePass123!', name: 'Alice');

// Log in -- returns JWT access token and refresh token
final session = await client.login('alice@example.com', 'SecurePass123!');
final accessToken = session['token'] as String;
final refreshTok = session['refresh_token'] as String;

// Set the JWT for subsequent authenticated calls
client.setAuthToken(accessToken);

// Refresh an expired token
final newSession = await client.refreshToken(refreshTok);
client.setAuthToken(newSession['token'] as String);

// Verify MFA (requires auth token already set)
final result = await client.verifyMfa(accessToken, '123456');

Events

// Send a single event
final event = await client.sendEvent('order.created', {'order_id': '123', 'amount': 99.99});

// Send batch events (up to 1000)
final batch = await client.sendEvents([
  {'topic': 'order.created', 'payload': {'order_id': '1'}},
  {'topic': 'order.created', 'payload': {'order_id': '2'}},
]);

// List events with pagination
final events = await client.listEvents(limit: 25);
final cursor = events['cursor'] as String?;
final nextPage = await client.listEvents(limit: 25, cursor: cursor);

// Get / delete a single event
final evt = await client.getEvent('evt_abc123');
await client.deleteEvent('evt_abc123');

Simulate

final result = await client.simulateEvent('order.created', {'order_id': 'test'});

Webhooks

// Create a webhook
final webhook = await client.createWebhook('https://example.com/webhook', extra: {'topics': ['order.*']});

// List, get, update, delete
final webhooks = await client.listWebhooks(limit: 50);
final wh = await client.getWebhook('wh_abc123');
await client.updateWebhook('wh_abc123', {'url': 'https://new-url.com/hook'});
await client.deleteWebhook('wh_abc123');

// Health and templates
final health = await client.webhookHealth('wh_abc123');
final templates = await client.webhookTemplates();

Deliveries

final deliveries = await client.listDeliveries(limit: 20, status: 'failed');
await client.retryDelivery('del_abc123');

Dead Letters

final deadLetters = await client.listDeadLetters(limit: 50);
final dl = await client.getDeadLetter('dlq_abc123');
await client.retryDeadLetter('dlq_abc123');
await client.resolveDeadLetter('dlq_abc123');

Replays

final replay = await client.createReplay(
  'order.created',
  '2026-01-01T00:00:00Z',
  '2026-01-31T23:59:59Z',
  webhookId: 'wh_abc123', // optional
);
final replays = await client.listReplays(limit: 50);
final r = await client.getReplay('rpl_abc123');
await client.cancelReplay('rpl_abc123');

Scheduled Jobs

// Create a job
final job = await client.createJob('daily-report', 'default', '0 9 * * *',
    extra: {'payload': {'type': 'daily'}});

// CRUD
final jobs = await client.listJobs(limit: 10);
final j = await client.getJob('job_abc123');
await client.updateJob('job_abc123', {'cron_expression': '0 10 * * *'});
await client.deleteJob('job_abc123');

// List runs for a job
final runs = await client.listJobRuns('job_abc123', limit: 20);

// Preview cron schedule
final preview = await client.cronPreview('0 9 * * *', count: 10);

Pipelines

final pipeline = await client.createPipeline('order-processing', ['order.created'], [
  {'type': 'filter', 'config': {'field': 'amount', 'gt': 100}},
  {'type': 'transform', 'config': {'add_field': 'priority', 'value': 'high'}},
]);

final pipelines = await client.listPipelines(limit: 50);
final p = await client.getPipeline('pipe_abc123');
await client.updatePipeline('pipe_abc123', {'name': 'order-processing-v2'});
await client.deletePipeline('pipe_abc123');

// Test a pipeline with a sample payload
final result = await client.testPipeline('pipe_abc123', {'topic': 'order.created', 'payload': {'id': '1'}});

Event Schemas

final schema = await client.createEventSchema('order.created', {
  'type': 'object',
  'properties': {
    'order_id': {'type': 'string'},
    'amount': {'type': 'number'},
  },
  'required': ['order_id', 'amount'],
});

final schemas = await client.listEventSchemas(limit: 50);
final s = await client.getEventSchema('sch_abc123');
await client.updateEventSchema('sch_abc123', {'schema': {'type': 'object'}});
await client.deleteEventSchema('sch_abc123');

// Validate a payload against a topic's schema
final result = await client.validatePayload('order.created', {'order_id': '123', 'amount': 50});

Sandbox

final endpoint = await client.createSandboxEndpoint(name: 'my-test');
final endpoints = await client.listSandboxEndpoints();
final requests = await client.listSandboxRequests('sbx_abc123', limit: 20);
await client.deleteSandboxEndpoint('sbx_abc123');

Analytics

final eventsChart = await client.eventsPerDay(days: 30);
final deliveriesChart = await client.deliveriesPerDay(days: 7);
final topics = await client.topTopics(limit: 5);
final stats = await client.webhookStats();

Project and Token Management

final project = await client.getProject();
await client.updateProject({'name': 'My Project v2'});
final topics = await client.listTopics();
final token = await client.getToken();
final newToken = await client.regenerateToken();

// Limit the token to scopes and client IPs (empty allowedIps = any IP)
await client.updateTokenRestrictions(scopes: ['events:read'], allowedIps: ['203.0.113.10']);

Multi-Project Management

final projects = await client.listProjects();
final newProject = await client.createProject('staging-env');
final p = await client.getProjectById('proj_abc123');
await client.updateProjectById('proj_abc123', {'name': 'production-env'});
await client.setDefaultProject('proj_abc123');
await client.deleteProject('proj_abc123');

Team Members

final members = await client.listMembers('proj_abc123');
final member = await client.addMember('proj_abc123', 'alice@example.com', role: 'admin');
await client.updateMember('proj_abc123', 'mem_abc123', 'viewer');
await client.removeMember('proj_abc123', 'mem_abc123');

Invitations

final invitations = await client.listPendingInvitations();
await client.acceptInvitation('inv_abc123');
await client.rejectInvitation('inv_def456');

Audit Logs

final logs = await client.listAuditLogs(limit: 100);

Data Export

import 'dart:io';

final csvData = await client.exportEvents(format: 'csv');
File('events.csv').writeAsStringSync(csvData);

final jsonData = await client.exportDeliveries(format: 'json');
await client.exportJobs(format: 'csv');
await client.exportAuditLog(format: 'csv');

GDPR / Privacy

final consents = await client.getConsents();
await client.acceptConsent('marketing');
final myData = await client.exportMyData();
await client.restrictProcessing();
await client.liftRestriction();
await client.objectToProcessing();
await client.restoreConsent();

Health Check

final health = await client.health();
final status = await client.status();

Error Handling

try {
  final event = await client.getEvent('nonexistent');
} on JobcelisException catch (e) {
  print('Status: ${e.statusCode}');
  print('Detail: ${e.detail}');
} catch (e) {
  print('Error: $e');
}

Webhook Signature Verification

import 'package:jobcelis/jobcelis.dart';

final body = '{"topic":"order.created"}';
final signature = '...';

if (WebhookVerifier.verify('your_webhook_secret', body, signature)) {
  print('Valid signature!');
} else {
  print('Invalid signature!');
}

License

BSL-1.1 (Business Source License)

Releasing

Merge to main: the Release workflow publishes every change to the package as the next patch version (1.3.1 → 1.3.2) to pub.dev (the Publish workflow publishes the tag), writes the version into pubspec.yaml and tags the commit. Changes to docs, tests or CI publish nothing. For a minor or major version, raise it in pubspec.yaml in the same pull request. Every pull request runs CI.

Libraries

jobcelis
Official Dart/Flutter SDK for the Jobcelis Event Infrastructure Platform.