README.md

# Jobcelis

Official Elixir SDK for the [Jobcelis](https://jobcelis.com) 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`:

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

Then run:

```bash
mix deps.get
```

## Quick Start

```elixir
# 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:
> ```elixir
> 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.

```elixir
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

```elixir
# 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

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

## Webhooks

```elixir
# 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

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

## Dead Letters

```elixir
{: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

```elixir
{: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

```elixir
# 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

```elixir
{: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

```elixir
{: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

```elixir
# 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

```elixir
{: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

```elixir
# 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)
```

## Multi-Project Management

```elixir
{: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

```elixir
{: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

```elixir
# 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

```elixir
{: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).

```elixir
# 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

```elixir
# 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

```elixir
{: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.

```elixir
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

```elixir
# 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

MIT