{"components":{"responses":{},"schemas":{"CronJob":{"example":{"active":true,"body":{"type":"daily"},"cron_expression":"0 9 * * 1-5","id":1,"inserted_at":"2026-01-01T00:00:00Z","logic_id":"daily-report-cron-001","max_attempts":3,"name":"daily-report","updated_at":"2026-01-01T00:00:00Z","user_id":1,"webhook_secret":"abc123xyz...","webhook_url":"https://example.com/hooks/report"},"properties":{"active":{"description":"Whether this cron is currently scheduled (false = paused)","type":"boolean"},"body":{"description":"JSON payload sent on each execution","type":"object"},"cron_expression":{"description":"5-field cron expression","type":"string"},"id":{"description":"Internal cron job ID","type":"integer"},"inserted_at":{"format":"date-time","type":"string"},"logic_id":{"description":"Client-defined unique identifier (scoped per user)","type":"string"},"max_attempts":{"description":"Max retry attempts per execution","type":"integer"},"name":{"description":"Human-readable name","type":"string"},"updated_at":{"format":"date-time","type":"string"},"user_id":{"description":"Owning user ID","type":"integer"},"webhook_secret":{"description":"Secret used to sign webhook payloads. The signature is sent in the X-JobsDone-Signature header as sha256=<hex_digest>.","nullable":true,"type":"string"},"webhook_url":{"description":"Target webhook URL","format":"uri","type":"string"}},"required":["id","logic_id","name","cron_expression","webhook_url","body","max_attempts","active","user_id","inserted_at","updated_at"],"title":"CronJob","type":"object"},"CronJobCreateRequest":{"example":{"body":{"type":"daily"},"cron_expression":"0 9 * * 1-5","logic_id":"daily-report-cron-001","max_attempts":3,"name":"daily-report","webhook_url":"https://example.com/hooks/report"},"properties":{"body":{"description":"Arbitrary JSON payload sent as the POST request body","nullable":true,"type":"object"},"cron_expression":{"description":"Standard 5-field cron expression (e.g. `* * * * *`, `0 9 * * 1-5`). Also accepts @hourly, @daily, @weekly, @monthly.","type":"string"},"logic_id":{"description":"Client-defined unique identifier for this cron job (scoped per user). Used as the primary ID for all API operations.","maxLength":255,"minLength":1,"type":"string"},"max_attempts":{"default":3,"description":"Number of retry attempts if the webhook call fails (1–100)","maximum":100,"minimum":1,"type":"integer"},"name":{"description":"Human-readable name for this cron job (scoped per user)","maxLength":255,"minLength":1,"type":"string"},"webhook_secret":{"description":"Secret used to sign webhook payloads. If not provided, a secret is auto-generated. The signature is sent in the X-JobsDone-Signature header as sha256=<hex_digest>.","nullable":true,"type":"string"},"webhook_url":{"description":"URL that will receive a POST request on each cron tick","format":"uri","maxLength":2048,"type":"string"}},"required":["logic_id","name","cron_expression","webhook_url"],"title":"CronJobCreateRequest","type":"object"},"CronJobResponse":{"properties":{"data":{"$ref":"#/components/schemas/CronJob"}},"required":["data"],"title":"CronJobResponse","type":"object"},"CronJobUpdateRequest":{"example":{"cron_expression":"0 10 * * 1-5","max_attempts":5},"properties":{"active":{"description":"Set to false to pause, true to resume","type":"boolean"},"body":{"nullable":true,"type":"object"},"cron_expression":{"type":"string"},"max_attempts":{"maximum":100,"minimum":1,"type":"integer"},"name":{"maxLength":255,"minLength":1,"type":"string"},"webhook_secret":{"description":"Secret used to sign webhook payloads. If not provided, a secret is auto-generated. The signature is sent in the X-JobsDone-Signature header as sha256=<hex_digest>.","nullable":true,"type":"string"},"webhook_url":{"format":"uri","maxLength":2048,"type":"string"}},"title":"CronJobUpdateRequest","type":"object"},"CronJobsResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/CronJob"},"type":"array"}},"required":["data"],"title":"CronJobsResponse","type":"object"},"CronStats":{"example":{"available":0,"cancelled":0,"completed":58,"cron_job_id":3,"discarded":2,"executing":0,"name":"daily-report","retryable":0,"scheduled":0,"success_rate":0.9667,"total":60},"properties":{"available":{"description":"Jobs waiting to be executed","type":"integer"},"cancelled":{"description":"Jobs that were manually cancelled","type":"integer"},"completed":{"description":"Successfully completed jobs","type":"integer"},"cron_job_id":{"description":"ID of the cron job","type":"integer"},"discarded":{"description":"Jobs that exhausted all retries and were discarded","type":"integer"},"executing":{"description":"Jobs currently executing","type":"integer"},"name":{"description":"Name of the cron job","type":"string"},"retryable":{"description":"Jobs scheduled for retry after failure","type":"integer"},"scheduled":{"description":"Jobs scheduled for a future execution time","type":"integer"},"success_rate":{"description":"Ratio of completed / (completed + discarded). Null when no terminal jobs exist.","format":"float","nullable":true,"type":"number"},"total":{"description":"Total jobs across all states","type":"integer"}},"required":["cron_job_id","name","total","completed","discarded","available","executing","retryable","scheduled","cancelled","success_rate"],"title":"CronStats","type":"object"},"EnqueueRequest":{"example":{"description":{"body":{"key":"value"},"webhook":"https://example.com/hook"},"logic_id":"send-welcome-email-001","scheduler_opts":{"max_attempts":3,"queue":"default"}},"properties":{"description":{"description":"Arbitrary JSON args forwarded to the worker's perform/1. Must include `webhook` and optionally `body` and `webhook_secret` for HMAC signature verification.","example":{"body":{},"webhook":"https://example.com/hook","webhook_secret":"optional-secret"},"type":"object"},"logic_id":{"description":"Client-defined unique identifier for idempotent enqueue. If a job with this logic_id already exists for this user, returns the existing job.","maxLength":255,"minLength":1,"type":"string"},"scheduler_opts":{"description":"Oban job options","nullable":true,"properties":{"max_attempts":{"description":"Maximum retry attempts","minimum":1,"type":"integer"},"priority":{"description":"Job priority — lower number is higher priority","maximum":9,"minimum":0,"type":"integer"},"queue":{"description":"Target Oban queue (default: \"default\")","type":"string"},"schedule_in":{"description":"Seconds from now to delay execution","minimum":1,"type":"integer"},"tags":{"description":"Arbitrary tags attached to the job","items":{"type":"string"},"type":"array"}},"type":"object"}},"required":["logic_id","description"],"title":"EnqueueRequest","type":"object"},"EnqueueResponse":{"example":{"data":{"id":42,"logic_id":"send-welcome-email-001","state":"available"}},"properties":{"data":{"properties":{"id":{"description":"Oban job ID","type":"integer"},"logic_id":{"description":"Client-defined unique identifier","type":"string"},"state":{"description":"Initial job state","enum":["available","scheduled"],"type":"string"}},"required":["id","logic_id","state"],"type":"object"}},"required":["data"],"title":"EnqueueResponse","type":"object"},"Error":{"example":{"error":"cron job not found"},"properties":{"error":{"description":"Human-readable error message","type":"string"}},"required":["error"],"title":"Error","type":"object"},"JobStats":{"example":{"available":2,"cancelled":0,"completed":115,"discarded":3,"executing":0,"retryable":0,"scheduled":0,"success_rate":0.9745,"total":120},"properties":{"available":{"description":"Jobs waiting to be executed","type":"integer"},"cancelled":{"description":"Jobs that were manually cancelled","type":"integer"},"completed":{"description":"Successfully completed jobs","type":"integer"},"discarded":{"description":"Jobs that exhausted all retries and were discarded","type":"integer"},"executing":{"description":"Jobs currently executing","type":"integer"},"retryable":{"description":"Jobs scheduled for retry after failure","type":"integer"},"scheduled":{"description":"Jobs scheduled for a future execution time","type":"integer"},"success_rate":{"description":"Ratio of completed / (completed + discarded). Null when no terminal jobs exist.","format":"float","nullable":true,"type":"number"},"total":{"description":"Total jobs across all states","type":"integer"}},"required":["total","completed","discarded","available","executing","retryable","scheduled","cancelled","success_rate"],"title":"JobStats","type":"object"},"ObanJob":{"properties":{"args":{"description":"Worker arguments","type":"object"},"attempt":{"description":"Current attempt number","type":"integer"},"attempted_at":{"format":"date-time","nullable":true,"type":"string"},"cancelled_at":{"format":"date-time","nullable":true,"type":"string"},"completed_at":{"format":"date-time","nullable":true,"type":"string"},"discarded_at":{"format":"date-time","nullable":true,"type":"string"},"errors":{"description":"Error details from previous failed attempts","items":{"type":"object"},"type":"array"},"id":{"description":"Job ID","type":"integer"},"inserted_at":{"format":"date-time","type":"string"},"logic_id":{"description":"Client-defined unique identifier (null for legacy jobs)","nullable":true,"type":"string"},"max_attempts":{"description":"Maximum allowed attempts","type":"integer"},"meta":{"description":"Job metadata (includes user_id, cron_job_id)","type":"object"},"queue":{"description":"Queue name","type":"string"},"scheduled_at":{"format":"date-time","type":"string"},"state":{"description":"Current job state","enum":["available","scheduled","executing","retryable","completed","discarded","cancelled"],"type":"string"},"worker":{"description":"Worker module name","type":"string"}},"required":["id","queue","worker","state","logic_id","args","meta","attempt","max_attempts","errors"],"title":"ObanJob","type":"object"},"ObanJobResponse":{"properties":{"data":{"$ref":"#/components/schemas/ObanJob"}},"required":["data"],"title":"ObanJobResponse","type":"object"},"ObanJobsResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/ObanJob"},"type":"array"}},"required":["data"],"title":"ObanJobsResponse","type":"object"},"User":{"example":{"api_key":"abc123xyz","id":1,"inserted_at":"2026-01-01T00:00:00Z","name":"Acme Corp"},"properties":{"api_key":{"description":"Opaque API key — include in X-API-Key header for all authenticated requests","type":"string"},"id":{"description":"User ID","type":"integer"},"inserted_at":{"description":"Creation timestamp (UTC)","format":"date-time","type":"string"},"name":{"description":"Display name","type":"string"}},"required":["id","name","api_key","inserted_at"],"title":"User","type":"object"},"UserCreateRequest":{"example":{"name":"Acme Corp"},"properties":{"name":{"description":"Display name for the user / application","maxLength":255,"minLength":1,"type":"string"}},"required":["name"],"title":"UserCreateRequest","type":"object"},"UserResponse":{"example":{"data":{"api_key":"abc123xyz","id":1,"inserted_at":"2026-01-01T00:00:00Z","name":"Acme Corp"}},"properties":{"data":{"$ref":"#/components/schemas/User"}},"required":["data"],"title":"UserResponse","type":"object"},"UserStatsResponse":{"properties":{"data":{"properties":{"crons":{"description":"Per-cron breakdown","items":{"$ref":"#/components/schemas/CronStats"},"type":"array"},"overall":{"$ref":"#/components/schemas/JobStats"},"user":{"$ref":"#/components/schemas/User"}},"required":["user","overall","crons"],"type":"object"}},"required":["data"],"title":"UserStatsResponse","type":"object"},"ValidationErrors":{"example":{"errors":{"cron_expression":["is invalid: expected 5 fields"],"name":["can't be blank"]}},"properties":{"errors":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"description":"Map of field names to lists of error messages","type":"object"}},"required":["errors"],"title":"ValidationErrors","type":"object"}},"securitySchemes":{"ApiKey":{"description":"API key obtained from POST /api/users","in":"header","name":"X-API-Key","type":"apiKey"}}},"info":{"description":"Multi-tenant job scheduler API.\n\nManage cron jobs and one-off enqueued jobs for your application.\nEach user is identified by an API key passed in the `X-API-Key` header.\n","title":"JobsDone API","version":"1.0.0"},"openapi":"3.0.0","paths":{"/api/crons":{"get":{"callbacks":{},"description":"Returns all cron jobs belonging to the authenticated user.","operationId":"JobsDoneWeb.CronController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CronJobsResponse"}}},"description":"List of cron jobs"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"}},"security":[{"ApiKey":[]}],"summary":"List cron jobs","tags":["Cron Jobs"]},"post":{"callbacks":{},"description":"Creates a new persisted cron job. The job starts scheduling immediately if `active` is true (default).","operationId":"JobsDoneWeb.CronController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CronJobCreateRequest"}}},"description":"Cron job params","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CronJobResponse"}}},"description":"Cron job created"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrors"}}},"description":"Validation errors"}},"security":[{"ApiKey":[]}],"summary":"Create cron job","tags":["Cron Jobs"]}},"/api/crons/{id}":{"delete":{"callbacks":{},"description":"Permanently deletes a cron job. The job stops scheduling immediately.","operationId":"JobsDoneWeb.CronController.delete","parameters":[{"description":"Cron job logic_id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Cron job deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Cron job not found"}},"security":[{"ApiKey":[]}],"summary":"Delete cron job","tags":["Cron Jobs"]},"get":{"callbacks":{},"description":"Returns a single cron job by logic_id (scoped to the authenticated user).","operationId":"JobsDoneWeb.CronController.show","parameters":[{"description":"Cron job logic_id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CronJobResponse"}}},"description":"Cron job"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Cron job not found"}},"security":[{"ApiKey":[]}],"summary":"Get cron job","tags":["Cron Jobs"]},"patch":{"callbacks":{},"description":"Updates one or more fields of an existing cron job. Only supplied fields are changed.","operationId":"JobsDoneWeb.CronController.update","parameters":[{"description":"Cron job logic_id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CronJobUpdateRequest"}}},"description":"Cron job update params","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CronJobResponse"}}},"description":"Cron job updated"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Cron job not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrors"}}},"description":"Validation errors"}},"security":[{"ApiKey":[]}],"summary":"Update cron job","tags":["Cron Jobs"]}},"/api/crons/{id}/pause":{"patch":{"callbacks":{},"description":"Sets `active = false`. The cron definition is preserved but will not fire until resumed.","operationId":"JobsDoneWeb.CronController.pause","parameters":[{"description":"Cron job logic_id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CronJobResponse"}}},"description":"Cron job paused"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Cron job not found"}},"security":[{"ApiKey":[]}],"summary":"Pause cron job","tags":["Cron Jobs"]}},"/api/crons/{id}/resume":{"patch":{"callbacks":{},"description":"Sets `active = true`. The cron will fire again on the next matching tick.","operationId":"JobsDoneWeb.CronController.resume","parameters":[{"description":"Cron job logic_id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CronJobResponse"}}},"description":"Cron job resumed"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Cron job not found"}},"security":[{"ApiKey":[]}],"summary":"Resume cron job","tags":["Cron Jobs"]}},"/api/enqueue":{"post":{"callbacks":{},"description":"Enqueues a one-off webhook job via Oban. The job is idempotent based on `logic_id` —\nif a job with the same logic_id already exists for this user, the existing job is returned.\n","operationId":"JobsDoneWeb.SchedulerController.enqueue","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnqueueRequest"}}},"description":"Enqueue params","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnqueueResponse"}}},"description":"Job enqueued"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing logic_id"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrors"}}},"description":"Validation errors"}},"security":[{"ApiKey":[]}],"summary":"Enqueue a one-off job","tags":["Jobs"]}},"/api/jobs":{"get":{"callbacks":{},"description":"Returns all Oban jobs belonging to the authenticated user. Supports filtering by logic_id (SQL LIKE pattern).","operationId":"JobsDoneWeb.UserController.list_jobs","parameters":[{"description":"Filter by job state","in":"query","name":"state","required":false,"schema":{"enum":["available","scheduled","executing","retryable","completed","discarded","cancelled"],"type":"string"}},{"description":"Filter by logic_id using SQL LIKE pattern (e.g. 'send_message_%')","in":"query","name":"logic_id","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ObanJobsResponse"}}},"description":"List of jobs"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"}},"security":[{"ApiKey":[]}],"summary":"List enqueued jobs","tags":["Users"]}},"/api/jobs/{id}":{"delete":{"callbacks":{},"description":"Cancels a one-off job by its logic_id.","operationId":"JobsDoneWeb.SchedulerController.delete","parameters":[{"description":"Job logic_id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Job cancelled"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Job not found"}},"security":[{"ApiKey":[]}],"summary":"Cancel a job","tags":["Jobs"]},"get":{"callbacks":{},"description":"Returns a single Oban job by logic_id (scoped to the authenticated user).","operationId":"JobsDoneWeb.UserController.show_job","parameters":[{"description":"Job logic_id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ObanJobResponse"}}},"description":"Job details"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Job not found"}},"security":[{"ApiKey":[]}],"summary":"Get enqueued job","tags":["Users"]}},"/api/users":{"post":{"callbacks":{},"description":"Registers a new user and returns their generated API key.","operationId":"JobsDoneWeb.UserController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCreateRequest"}}},"description":"User creation params","required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}},"description":"User created"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrors"}}},"description":"Validation errors"}},"summary":"Create user","tags":["Users"]}},"/api/users/me":{"get":{"callbacks":{},"description":"Returns the authenticated user's profile.","operationId":"JobsDoneWeb.UserController.me","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}},"description":"Current user"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"}},"security":[{"ApiKey":[]}],"summary":"Get current user","tags":["Users"]}},"/api/users/me/stats":{"get":{"callbacks":{},"description":"Returns overall job success rates plus a per-cron breakdown.\n\n`success_rate` is `completed / (completed + discarded)`, or `null` when\nno terminal jobs exist yet.\n","operationId":"JobsDoneWeb.UserController.stats","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserStatsResponse"}}},"description":"User stats"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid or missing API key"}},"security":[{"ApiKey":[]}],"summary":"Get user stats","tags":["Users"]}}},"security":[],"servers":[{"url":"https://jobsdone.claudlabs.com","variables":{}}],"tags":[]}