# JetDeploy > JetDeploy runs your apps and your data services on a managed Kubernetes cluster without asking you to learn Kubernetes. You push your code with git, JetDeploy builds it into a container image, deploys it behind HTTPS on a host of its own and keeps it running, next to managed PostgreSQL, MariaDB, Redis, OpenSearch and RabbitMQ services. Everything the web console does is also a JSON API under https://jetdeploy.com/api/v1, so a script or an agent can create an app, configure it, deploy it, attach a domain, read its logs and run one-off commands in it without opening a browser. ## API - [OpenAPI schema](https://jetdeploy.com/api/v1/openapi.json): every path, parameter, field and error of the API. Public, no token needed. - [Interactive API docs](https://jetdeploy.com/api/v1/docs): the same schema, browsable and callable. - [MCP server](https://jetdeploy.com/mcp): the same operations as MCP tools, a tool for every API operation except the live streams, the payment methods, the token regenerations and the log histogram/context parts, over plain HTTP; add it in claude.ai under Settings > Connectors > Add custom connector, in ChatGPT developer mode, or with `claude mcp add --transport http jetdeploy https://jetdeploy.com/mcp`. It authenticates with OAuth; the `create_app` and `get_app` tools return `git_remote_with_token`, the git remote with the Git Access Token in it, for the push step, and it, like `git_token.secret` in `get_profile`, is returned only to a connection with the `write` scope. Connected agents are listed at `GET /api/v1/profile/connections` and by the `list_connections` tool, and revoked at `DELETE /api/v1/profile/connections/{id}` or from the console page Integrations > AI Agents & MCP, never by a tool. See https://jetdeploy.com/support/docs#api-agent. - [API guide](https://jetdeploy.com/support/docs#api): what the API is, with curl examples for every step below. - [Authentication](https://jetdeploy.com/support/docs#api-auth): send `Authorization: Bearer `; the token is in the console under Integrations > API Access Token. The Git Access Token is a different token, used only as the password of the git remote for `git push`; `GET /api/v1/profile` returns it in clear as `git_token.secret`, so the API token alone is enough for an agent. - [Organizations](https://jetdeploy.com/support/docs#api-organizations): list and create endpoints take `?organization=` and default to the current organization of the user; `GET /api/v1/organizations` lists the ones you belong to. - [Errors](https://jetdeploy.com/support/docs#api-errors): every error is `{"code": ..., "message": ..., "hint": ...}`, with an `errors` object per field on validation errors. - [Operations](https://jetdeploy.com/support/docs#api-operations): deploy, apply, restart, start, stop, expose and destroy answer `202` with an operation; poll `GET /api/v1/operations/{id}` until `is_finished`. A second action on the same target while one runs is refused with `409 operation_pending`. `POST /api/v1/operations/{id}/cancel` stops one that has not finished. Before the pre-deploy script starts, Cancel stops the deploy cleanly. While the script runs, Cancel stops it within seconds, wherever it got to (on a database without transactional DDL, such as MariaDB or MySQL, a migration can be left half done), and the new version is never rolled out. Once the script has finished, the new version is already rolling out and Cancel cannot undo it: it only ends the operation, so that another deploy can start, and a deploy whose rollout is already complete is reported as succeeded. - [Commands and log tails](https://jetdeploy.com/support/docs#api-streams): the exec and the log tails answer `application/x-ndjson`, one JSON object per line, and the last line carries `exit_code` or `end`. Both have a server-side timeout, which ends the stream only: an exec command is not killed and may keep running in the container, so wrap it with the `timeout` utility of the image when it has to stop. To keep following a log tail, call it again with `?cursor=` set to the `cursor` of its end line, or of the last line handled when the stream broke off, passed back as it is (it can be ahead of the last line sent): a line that reaches the log store up to 15 seconds late (60 on the deploy tail) is still delivered while the tail keeps up with the logs; a later one, or one a few seconds late while the tail catches up on a dense stretch (several hundred lines per second), may be missed and a page read afterwards has it. The first lines can repeat those of the 15 (60) seconds before the cursor: keep the `key` of the lines of that stretch and skip the ones already handled. Follow with a long `timeout`: short tails on dense logs repeat more lines and fall behind. Bound a tail with `timeout`, not with a line count: the lines sharing a nanosecond can carry the same `cursor`, so a client that stops after a fixed count may never get past a larger group; one that counts has to count only the lines whose `key` is new to it. The deploy tail ends at its outcome line: lines that reach the log store after it, or late while the tail was catching up to it, are only in the pages, and a resumed tail sends the outcome line again past the cursor, so skip it by `key` and resume from its `cursor`, not its `ns`. For already-ingested lines, page instead: `GET .../runtime-logs`, `.../deploy-logs` or `/api/v1/services/{id}/logs` answer `{"rows": [...], "next_cursor", "prev_cursor", "window": {"from", "to"}}`, rows oldest first with `ns`, `timestamp`, `level`, `message`, `pod`, `container`, `key` (the `key` a tail read with the same filters sends for the line; a `field:value` search changes it; deploy-logs rows add `stream`); each cursor is `:` from a row of the page. Page with `?cursor=next_cursor` to keep the same `?direction=` (default `backward`, older first); to read the other way, pass `?cursor=prev_cursor` together with the opposite `?direction=` — without flipping it, `prev_cursor` only re-reads an overlapping page. With a search, an empty page can come back before the end of the lines; keep paging while `next_cursor` changes. - [Rate limits](https://jetdeploy.com/support/docs#api-limits): per token per minute; over the limit the answer is `429` with a `Retry-After` header. At most 8 commands and log tails can be open at once per token; beyond that the answer is `429` with code `too_many_streams` until one ends. ## How an agent deploys 1. Create the app: `POST /api/v1/apps` with `{"name": "my-app", "branch": "main"}`. The answer carries the app `id`, its `git_remote_url`, its `url` and a `next_step`. 2. Set the configuration: `PUT /api/v1/apps/{app_id}/envs` with `{"envs": {"KEY": "value"}}`, which replaces the whole set of environment variables; `GET` and `PUT` both answer the flat map `{"KEY": "value", ...}` (not wrapped, even though the `PUT` body is). `POST /api/v1/apps/{app_id}/envs` with `{"key", "value"}`, and `GET`/`PATCH` on `.../envs/{key}`, answer `{"key", "value", "apply_required"}`; `DELETE` on `.../envs/{key}` answers `204` — every value comes back in clear, secrets included, so mask it before printing or logging. 3. Add the process: `POST /api/v1/apps/{app_id}/pods` with `{"name": "web", "port": 8080, "external": true}`. The edge terminates HTTPS and forwards plain HTTP to it (redirecting `http://` requests before they arrive), and sets `X-Forwarded-Proto: https` on every request, which the client cannot spoof; trust that header instead of the connection, e.g. Django `SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")` and leave `SECURE_SSL_REDIRECT` off. 4. Set the pre-deploy script now, if you need one, before the push below: `PATCH /api/v1/apps/{app_id}` with `{"predeploy_script": "python manage.py migrate --noinput"}`. It runs after the build and before the new version gets traffic, inside the built image with the app's env vars. A failing script is retried once, so write it to be safe to run twice. The script may take at most 510 seconds, both attempts included, and at most 600 seconds together with the start of the new version; the build before it has its own 1200 seconds. If the script fails twice or runs out of time, the deploy fails and the previous version, if there is one, keeps serving; past the time limit the pre-deploy is stopped wherever it got to, even if its container never started, and a script stopped halfway can leave a MariaDB or MySQL database half migrated. Run a step that needs longer outside the script: deploy without it, run the step once in the new version with `POST /api/v1/apps/{app_id}/pods/{pod_id}/exec`, then set the script again; until it finishes the new version runs against the old schema, so the migration has to be backward compatible. Once the app has a pod, a push starts the deploy itself, so this has to be set first. The script runs in a container of its own that shares no files with the processes: file-producing steps such as collectstatic belong in the Dockerfile, and the build does not see the app's env vars. 5. Push the code to `git_remote_url` with `git` as the username and the Git Access Token as the password, on the branch of the app. Read the token from `GET /api/v1/profile` (`git_token.secret`) with the API token alone. Never store the token: pass it through a one-shot credential helper reading an env var, resetting the helper list first (`-c credential.helper=`) so git does not fall back to a stale stored credential, e.g. `git -c credential.helper= -c credential.helper='!f() { echo username=git; echo "password=$JD_GIT_TOKEN"; }; f' push HEAD:refs/heads/`. This is the only step that is not an API call; when the app already has a pod, the push itself starts the build and the deploy. 6. Deploy only when the push did not start one (the pod was added after the push, or to redeploy): `POST /api/v1/apps/{app_id}/deploy`; a `409 operation_pending` means it is already running, wait for it instead. 7. Poll `GET /api/v1/operations/{operation_id}` until `is_finished` is true and `status` is `SUCCESS`; read the build output meanwhile at `GET /api/v1/apps/{app_id}/deploy-logs/tail`. 8. The app answers at its `url`. Tail the runtime logs at `GET /api/v1/apps/{app_id}/runtime-logs/tail`, and run one-off commands with `POST /api/v1/apps/{app_id}/pods/{pod_id}/exec`. Add a data service in one call with `POST /api/v1/services` (`storage_size` in GiB goes either at the top level or inside `attrs`, never both, or `400 storage_size_twice`; Redis takes neither — its volume is sized from `attrs.memory` in MB, a top-level `storage_size` is ignored and one inside `attrs` is refused with `400 unknown_attribute`); it answers at once, in `created` status, with `credentials`, a `pending_operation` for the deploy just queued and everything else `GET /api/v1/services/{service_id}` also carries, `internal_host`, `port` and `storage_size` (for Redis, computed from `memory`; null while the service has no volume) — both answers carry the password in clear, mask `credentials` before printing or logging them. The service is ready to use once `status` is `ready`, usually about a minute after the create; if `pending_operation` turns null first, the deploy failed (`deploy_failed` is `true`): retry it with `POST /api/v1/services/{service_id}/deploy`. An app that needs it at deploy time, such as a pre-deploy migration, should wait for `ready` before its first push. Nothing injects these into an app automatically, set them yourself with `PUT /api/v1/apps/{app_id}/envs`, plain or composed into a `DATABASE_URL` — a generated password is letters and digits only, so it needs no percent-encoding. Claim a domain with `POST /api/v1/domains`, create the DNS records the answer asks for, validate it with `POST /api/v1/domains/{domain_id}/validate`, attach it with `POST /api/v1/apps/{app_id}/domains` and apply the app with `POST /api/v1/apps/{app_id}/apply`. Switching the routing mode with `PATCH /api/v1/domains/{domain_id}` makes a validated domain unvalidated, so validate it and apply the app again. - [Blog](https://jetdeploy.com/blog/): release notes and how-tos. - [Terms and conditions](https://jetdeploy.com/legal/terms-and-conditions) - [Privacy policy](https://jetdeploy.com/legal/privacy-policy) - Support: hello@jetdeploy.com