Jobcelis

Official Elixir 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

Add jobcelis to your list of dependencies in mix.exs:

def deps do
  [
    {:jobcelis, "~> 1.0"}
  ]
end

Then run:

mix deps.get

Quick Start

# Only your API key is required -- connects to https://jobcelis.com automatically
client = Jobcelis.client("your_api_key")

{:ok, event} = Jobcelis.send_event(client, "order.created", %{order_id: "123", amount: 99.99})

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

client = Jobcelis.client("your_api_key", base_url: "https://your-instance.example.com")

Authentication

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

client = Jobcelis.client("")

# Register a new account
{:ok, user} = Jobcelis.register(client, "alice@example.com", "SecurePass123!", name: "Alice")

# Log in -- returns JWT access token and refresh token
{:ok, session} = Jobcelis.login(client, "alice@example.com", "SecurePass123!")
access_token = session["token"]
refresh_token = session["refresh_token"]

# Set the JWT for subsequent authenticated calls
client = Jobcelis.set_auth_token(client, access_token)

# Refresh an expired token
{:ok, new_session} = Jobcelis.refresh_token(client, refresh_token)
client = Jobcelis.set_auth_token(client, new_session["token"])

# Verify MFA (requires auth token already set)
{:ok, result} = Jobcelis.verify_mfa(client, access_token, "123456")

Events

# Send a single event
{:ok, event} = Jobcelis.send_event(client, "order.created", %{order_id: "123", amount: 99.99})

# Send batch events (up to 1000)
{:ok, batch} = Jobcelis.send_events(client, [
  %{topic: "order.created", payload: %{order_id: "1"}},
  %{topic: "order.created", payload: %{order_id: "2"}}
])

# List events with pagination
{:ok, events} = Jobcelis.list_events(client, limit: 25)
{:ok, next_page} = Jobcelis.list_events(client, limit: 25, cursor: events["cursor"])

# Get / delete a single event
{:ok, event} = Jobcelis.get_event(client, "evt_abc123")
:ok = Jobcelis.delete_event(client, "evt_abc123")

Simulate

# Dry-run an event to see which webhooks would fire
{:ok, result} = Jobcelis.simulate_event(client, "order.created", %{order_id: "test"})

Webhooks

# Create a webhook
{:ok, webhook} = Jobcelis.create_webhook(client, "https://example.com/webhook",
  topics: ["order.*"]
)

# List, get, update, delete
{:ok, webhooks} = Jobcelis.list_webhooks(client)
{:ok, wh} = Jobcelis.get_webhook(client, "wh_abc123")
{:ok, _} = Jobcelis.update_webhook(client, "wh_abc123", %{url: "https://new-url.com/hook"})
:ok = Jobcelis.delete_webhook(client, "wh_abc123")

# Health and templates
{:ok, health} = Jobcelis.webhook_health(client, "wh_abc123")
{:ok, templates} = Jobcelis.webhook_templates(client)

Deliveries

{:ok, deliveries} = Jobcelis.list_deliveries(client, limit: 20, status: "failed")
{:ok, _} = Jobcelis.retry_delivery(client, "del_abc123")

Dead Letters

{:ok, dead_letters} = Jobcelis.list_dead_letters(client)
{:ok, dl} = Jobcelis.get_dead_letter(client, "dlq_abc123")
{:ok, _} = Jobcelis.retry_dead_letter(client, "dlq_abc123")
{:ok, _} = Jobcelis.resolve_dead_letter(client, "dlq_abc123")

Replays

{:ok, replay} = Jobcelis.create_replay(client, "order.created",
  "2026-01-01T00:00:00Z",
  "2026-01-31T23:59:59Z",
  webhook_id: "wh_abc123"  # optional
)
{:ok, replays} = Jobcelis.list_replays(client)
{:ok, r} = Jobcelis.get_replay(client, "rpl_abc123")
:ok = Jobcelis.cancel_replay(client, "rpl_abc123")

Scheduled Jobs

# Create a job
{:ok, job} = Jobcelis.create_job(client, "daily-report", "default", "0 9 * * *",
  payload: %{type: "daily"}
)

# CRUD
{:ok, jobs} = Jobcelis.list_jobs(client, limit: 10)
{:ok, job} = Jobcelis.get_job(client, "job_abc123")
{:ok, _} = Jobcelis.update_job(client, "job_abc123", %{cron_expression: "0 10 * * *"})
:ok = Jobcelis.delete_job(client, "job_abc123")

# List runs for a job
{:ok, runs} = Jobcelis.list_job_runs(client, "job_abc123", limit: 20)

# Preview cron schedule
{:ok, preview} = Jobcelis.cron_preview(client, "0 9 * * *", count: 10)

Pipelines

{:ok, pipeline} = Jobcelis.create_pipeline(client, "order-processing",
  ["order.created"],
  [
    %{type: "filter", config: %{field: "amount", gt: 100}},
    %{type: "transform", config: %{add_field: "priority", value: "high"}}
  ]
)

{:ok, pipelines} = Jobcelis.list_pipelines(client)
{:ok, p} = Jobcelis.get_pipeline(client, "pipe_abc123")
{:ok, _} = Jobcelis.update_pipeline(client, "pipe_abc123", %{name: "order-processing-v2"})
:ok = Jobcelis.delete_pipeline(client, "pipe_abc123")

# Test a pipeline with a sample payload
{:ok, result} = Jobcelis.test_pipeline(client, "pipe_abc123", %{
  topic: "order.created",
  payload: %{id: "1"}
})

Event Schemas

{:ok, schema} = Jobcelis.create_event_schema(client, "order.created", %{
  type: "object",
  properties: %{
    order_id: %{type: "string"},
    amount: %{type: "number"}
  },
  required: ["order_id", "amount"]
})

{:ok, schemas} = Jobcelis.list_event_schemas(client)
{:ok, s} = Jobcelis.get_event_schema(client, "sch_abc123")
{:ok, _} = Jobcelis.update_event_schema(client, "sch_abc123", %{schema: %{type: "object"}})
:ok = Jobcelis.delete_event_schema(client, "sch_abc123")

# Validate a payload against a topic's schema
{:ok, result} = Jobcelis.validate_payload(client, "order.created", %{order_id: "123", amount: 50})

Sandbox

# Create a temporary endpoint for testing
{:ok, endpoint} = Jobcelis.create_sandbox_endpoint(client, name: "my-test")
{:ok, endpoints} = Jobcelis.list_sandbox_endpoints(client)

# Inspect received requests
{:ok, requests} = Jobcelis.list_sandbox_requests(client, "sbx_abc123", limit: 20)

:ok = Jobcelis.delete_sandbox_endpoint(client, "sbx_abc123")

Analytics

{:ok, events_chart} = Jobcelis.events_per_day(client, days: 30)
{:ok, deliveries_chart} = Jobcelis.deliveries_per_day(client, days: 7)
{:ok, topics} = Jobcelis.top_topics(client, limit: 5)
{:ok, stats} = Jobcelis.webhook_stats(client)

Project and Token Management

# Current project
{:ok, project} = Jobcelis.get_project(client)
{:ok, _} = Jobcelis.update_project(client, %{name: "My Project v2"})

# Topics
{:ok, topics} = Jobcelis.list_topics(client)

# API token
{:ok, token} = Jobcelis.get_token(client)
{:ok, new_token} = Jobcelis.regenerate_token(client)

# Limit the token to scopes and client IPs (empty allowed_ips = any IP)
{:ok, _} = Jobcelis.update_token_restrictions(client, scopes: ["events:read"], allowed_ips: ["203.0.113.10"])

Multi-Project Management

{:ok, projects} = Jobcelis.list_projects(client)
{:ok, new_project} = Jobcelis.create_project(client, "staging-env")
{:ok, p} = Jobcelis.get_project_by_id(client, "proj_abc123")
{:ok, _} = Jobcelis.update_project_by_id(client, "proj_abc123", %{name: "production-env"})
{:ok, _} = Jobcelis.set_default_project(client, "proj_abc123")
:ok = Jobcelis.delete_project(client, "proj_abc123")

Team Members

{:ok, members} = Jobcelis.list_members(client, "proj_abc123")
{:ok, member} = Jobcelis.add_member(client, "proj_abc123", "alice@example.com", role: "admin")
{:ok, _} = Jobcelis.update_member(client, "proj_abc123", "mem_abc123", "viewer")
:ok = Jobcelis.remove_member(client, "proj_abc123", "mem_abc123")

Invitations

# List pending invitations
{:ok, invitations} = Jobcelis.list_pending_invitations(client)

# Accept or reject
{:ok, _} = Jobcelis.accept_invitation(client, "inv_abc123")
{:ok, _} = Jobcelis.reject_invitation(client, "inv_def456")

Audit Logs

{:ok, logs} = Jobcelis.list_audit_logs(client, limit: 100)
{:ok, next_page} = Jobcelis.list_audit_logs(client, cursor: logs["cursor"])

Data Export

Export methods return raw binary (CSV or JSON).

# Export as CSV
{:ok, csv_data} = Jobcelis.export_events(client, format: "csv")
File.write!("events.csv", csv_data)

# Export as JSON
{:ok, json_data} = Jobcelis.export_deliveries(client, format: "json")

# Other exports
{:ok, _} = Jobcelis.export_jobs(client, format: "csv")
{:ok, _} = Jobcelis.export_audit_log(client, format: "csv")

GDPR / Privacy

# Consent management
{:ok, consents} = Jobcelis.get_consents(client)
{:ok, _} = Jobcelis.accept_consent(client, "marketing")

# Data portability
{:ok, my_data} = Jobcelis.export_my_data(client)

# Processing restrictions
{:ok, _} = Jobcelis.restrict_processing(client)
:ok = Jobcelis.lift_restriction(client)

# Right to object
{:ok, _} = Jobcelis.object_to_processing(client)
:ok = Jobcelis.restore_consent(client)

Health Check

{:ok, health} = Jobcelis.health(client)
{:ok, status} = Jobcelis.status(client)

Error Handling

All API calls return {:ok, result} on success or {:error, %Jobcelis.Error{}} on failure.

case Jobcelis.get_event(client, "nonexistent") do
  {:ok, event} ->
    IO.inspect(event)

  {:error, %Jobcelis.Error{status: 404, detail: detail}} ->
    IO.puts("Not found: #{inspect(detail)}")

  {:error, %Jobcelis.Error{status: status, detail: detail}} ->
    IO.puts("Error #{status}: #{inspect(detail)}")
end

Webhook Signature Verification

# In a Phoenix controller
def webhook(conn, _params) do
  {:ok, body, conn} = Plug.Conn.read_body(conn)
  signature = Plug.Conn.get_req_header(conn, "x-signature") |> List.first("")

  if Jobcelis.WebhookVerifier.verify("your_webhook_secret", body, signature) do
    event = Jason.decode!(body)
    IO.puts("Received: #{event["topic"]}")
    send_resp(conn, 200, "OK")
  else
    send_resp(conn, 401, "Invalid signature")
  end
end

License

BSL-1.1 (Business Source License)

Releasing

Bump the version in mix.exs and merge to main. The Release workflow publishes it to Hex.pm and tags the commit. Changes that do not bump the version publish nothing.