REST API
Pushify API Documentation
Deploy, manage and monitor applications from CI/CD pipelines, scripts and your own tools.
Resource IDs
Managed server billing
REST and JSON
Plain REST endpoints, JSON responses
Scoped keys
Each API key gets only the scopes you grant
Built for CI/CD
Webhook triggers and a deploy API
https://api.pushify.dev/api/v1Quick start#
- 01
Create an API key
Go to Settings → API Keys and create a key with the required scopes.
- 02
Make a request
Send the key in the Authorization header.
- 03
Automate
Call it from GitHub Actions, GitLab CI or any other CI/CD tool.
Explore#
Authentication#
Every request needs an API key, sent as a Bearer token in the Authorization header.
Authorization: Bearer pk_live_YOUR_API_KEYcurl -X GET "https://api.pushify.dev/api/v1/projects" \
-H "Authorization: Bearer pk_live_YOUR_API_KEY" \
-H "Content-Type: application/json"Scopes#
Give each key only the scopes it needs when you create it.
projects:read- List and view projects
projects:write- Create and update projects
projects:delete- Delete projects
deployments:read- View deployments and logs
deployments:write- Trigger, redeploy, rollback deploys
deployments:cancel- Cancel pending or running deployments
logs:read- Read deployment build and deploy logs
metrics:read- Read CPU, memory and network metrics
envvars:read- View environment variables
envvars:write- Manage environment variables
servers:read- View servers
servers:write- Manage servers
databases:read- View databases
databases:write- Manage databases
domains:read- View domains
domains:write- Manage domains
Security
Dashboard session only
Projects#
Create, configure, update and delete projects.
Deployments#
Trigger deployments, follow their logs, and roll back when needed.
Environment Variables#
Manage a project's environment variables. Changes take effect on the next deployment.
Sensitive values
Domains#
Add custom domains to a project. DNS verification, SSL certificates and the Nginx config are handled for you.
Servers#
Provision cloud servers, start, stop and reboot them, and check their status.
Not available via API key
Databases#
Run PostgreSQL, MySQL, Redis and MongoDB on your servers, with backups.
Webhooks & CI/CD#
Deploy on every push to GitHub: Pushify receives the webhook and starts a deployment.
How it works#
- 01
Connect GitHub
Link your GitHub account in project settings.
- 02
Push to the branch
Push code to the configured branch (e.g. main).
- 03
It deploys
Pushify receives the webhook and starts a deployment.
Manual webhook URL#
Every project has its own webhook URL, for wiring up other Git providers by hand.
POST https://api.pushify.dev/api/v1/webhooks/github/:projectId
Headers:
X-Hub-Signature-256: sha256=<HMAC signature>
Content-Type: application/json
Body:
{
"ref": "refs/heads/main",
"head_commit": {
"id": "abc123",
"message": "Deploy new feature"
}
}GitHub Actions#
Trigger a deployment from a workflow with the Deployments API.
# .github/workflows/deploy.yml
name: Deploy to Pushify
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Trigger deployment
run: |
curl -X POST "https://api.pushify.dev/api/v1/projects/${{ secrets.PROJECT_ID }}/deployments" \
-H "Authorization: Bearer ${{ secrets.PUSHIFY_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{
"branch": "main",
"commitHash": "${{ github.sha }}",
"commitMessage": "${{ github.event.head_commit.message }}"
}'Webhook secret
GET /projects/:id/webhook endpoint.Monitoring, logs and backups#
What Pushify watches, the thresholds that decide when it emails you, and how long it keeps what it collects.
When you get an email#
Every active project with a live deployment and a URL is called about once a minute — no configuration needed. These are the conditions that send mail:
- The app stops answering
- Three failed checks in a row, so a single restart is not an outage. A second email arrives when it answers again, saying how long it was out. Without a configured health check any answer counts as up — a 404 on / is a missing route, not a down app.
- Memory above 90% of the limit for 5 minutes
- Past this the kernel is choosing what to kill next, and the usual result is a restart loop. This is the warning that arrives before the app stops answering. Ignored where no memory limit is set, since the percentage would then be of the whole server.
- CPU above 90% for 15 minutes
- Much longer than memory, because a build or a batch job legitimately saturates a CPU. The app is not down — requests are queuing behind it.
- A server disk crosses its warning level
- Checked hourly, not just at deploy time. A full disk takes every container on the box down together, databases included. One reminder a day while it stays full.
- An HTTPS certificate is about to expire
- Fourteen days before, then once more three days before. Renewal is automatic until something stops it — DNS moved, port 80 blocked.
Why you are not flooded
Who gets them#
Everyone in the organization with deployment alerts turned on, in their own notification settings. A project's notification channels (Slack, Discord, webhooks) also receive the up/down events.
How long logs are kept#
Container output is collected from every container a project runs — the app, its replicas, its workers and its staging copy — and is searchable by term, time range and container. How long it stays depends on the plan:
| Plan | Kept for |
|---|---|
| Free | 3 days |
| Hobby | 7 days |
| Pro | 14 days |
| Business | 30 days |
| Enterprise | 90 days |
Scaling on load#
Project settings → Scale automatically gives a minimum and a maximum, and Pushify moves the container count between them as CPU changes. Pro plan and above. Left off, the count stays exactly where you set it.
- One container at a time: a reading of 100% CPU adds one, not five — a container takes time to start and the reading cannot yet know whether one more is enough.
- Added above 70% average CPU, and not again for three minutes. Removed below 30%, and not again for ten — a lull at lunchtime should not undo a busy morning.
- At least three readings before anything happens, so a single spike changes nothing.
- Changing the minimum or maximum applies immediately, without waiting for a threshold or a cooldown: the range is an instruction, the thresholds are a guess.
- A new container is started from what the last deploy actually used, so it cannot differ from the ones already running. A project deployed before autoscaling existed starts scaling after its next deploy.
- Scaling down takes the container out of nginx and reloads before stopping it, so requests already in flight finish.
- Every change is written down with the reading that caused it, and listed in project settings.
- Not sure the thresholds suit your app? Turn on "Only report what it would do": the decision still runs and is recorded, but nothing changes. Watch for a few days, then decide.
Backups and what an interval costs#
A managed database is backed up automatically. The interval you pick is your worst-case data loss: at 24 hours, losing the disk costs a day of writes. The plan sets the shortest interval available — a day on Free, 12 hours on Hobby, 6 on Pro, hourly on Business and Enterprise.
- A database that has never been backed up is backed up at once rather than waiting out its first interval.
- Where the operator has configured off-site storage, each dump is also copied off the server it was made on — and a restore falls back to that copy, so a database can be restored onto a server that has never seen the file.
- The backup list shows whether an off-site copy exists for each backup.
- Deleting a database deletes its backups, off-site copies included.
Private images and compose stacks#
A project can build a repository, run a ready image or bring up a compose stack. Here is what each needs, including the registry scopes that are actually required.
Private registries#
Settings → Private registries stores one login per registry for the whole organization. It is used for two things: a Dockerfile whose FROM is a private base image, and projects that deploy a ready image. The token is write-only — it is sent once and never shown again, only replaced.
GitHub Container Registry ghcr.io
- GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic).
- Generate a token with the read:packages scope. That one scope is enough to pull; repo and write:packages are not needed.
- Username is your GitHub username; password is the token.
Docker Hub docker.io
- Docker Hub → Account Settings → Personal access tokens → Generate.
- Give it Read-only access.
- Username is your Docker Hub username; password is the token, not your account password.
GitLab Container Registry registry.gitlab.com
- GitLab project → Settings → Repository → Deploy tokens.
- Create one with the read_registry scope.
- Use the token username and token value exactly as GitLab shows them.
Read-only is enough
Deploying a ready image#
Project settings → Docker image: fill in a reference and the project deploys that image instead of building the repository. Every deploy pulls the reference again, so moving a tag and redeploying ships the new image.
ghcr.io/acme/api:1.4- The image keeps everything it already declares — its CMD, ENV and exposed port are used as they are.
- It gets the same treatment as a built app: blue-green switch, replicas, staging, volumes, domains and HTTPS.
- The repository settings below the field stop applying, and the dashboard says so.
- A private image needs a registry credential for its host — see above.
- This needs a server; the no-server fallback cannot pull images.
Deploying a compose stack#
Project settings → Docker Compose file: give the path of a compose file in your repository and the project is deployed as a stack from your checkout, so build: contexts and the config files beside it work as they do locally. It is off by default and never picked up on its own — most repositories carry a compose file meant for local development, and deploying that would be a surprise.
services:
web:
build: ./web
ports:
- "8080:3000"
environment:
API: http://api:4000
api:
build: ./api- Services reach each other by name on the stack network, exactly as they do locally.
- When more than one service publishes a port, name the one to serve in the settings — otherwise the deploy is refused rather than guessing.
- The project's environment variables are available to the stack, and a committed .env is read first.
- Pushify's Workers and Scheduled tasks drive a single container and do not apply — declare those as services in the compose file. The deploy log says so rather than ignoring them.
- A redeploy takes the stack down and brings it up, so unlike a built app it is not zero-downtime.
Ports are decided for you
Single sign-on (OIDC)#
Let your team sign in through your own identity provider. What goes wrong is almost always on the provider's side, so this covers what to enter there.
Before you start#
You need to be the organization's owner. Open Settings → Single sign-on: it shows the redirect URI your provider must send people back to. Copy it now — every provider asks for it first.
https://api.pushify.dev/api/v1/sso/callbackThe redirect URI has to match exactly
Okta#
- In the Okta admin console, go to Applications → Create App Integration.
- Choose OIDC – OpenID Connect, then Web Application.
- Under Sign-in redirect URIs, paste the redirect URI from Pushify.
- Under Assignments, pick who may use it — only these people will be able to sign in.
- Save, then copy the Client ID and Client secret from the General tab.
https://YOUR-TENANT.okta.comMicrosoft Entra ID (Azure AD)#
- In the Azure portal, open Microsoft Entra ID → App registrations → New registration.
- For Redirect URI choose Web and paste the one from Pushify.
- After registering, note the Application (client) ID and the Directory (tenant) ID.
- Go to Certificates & secrets → New client secret and copy the secret Value (not the ID — the Value is only shown once).
- Under Token configuration, add the optional claim email, and tick the box to turn on the Microsoft Graph email permission if it offers.
https://login.microsoftonline.com/YOUR-TENANT-ID/v2.0Google Workspace#
- In Google Cloud Console, pick the project for your organization and open APIs & Services → Credentials.
- Create Credentials → OAuth client ID → Web application.
- Under Authorised redirect URIs, paste the one from Pushify.
- Copy the Client ID and Client secret.
- On the OAuth consent screen, set User type to Internal so only your Workspace accounts can use it.
https://accounts.google.comFinish in Pushify#
- Settings → Single sign-on: enter the issuer, client ID and client secret.
- Add the email domains your organization owns — only addresses in these sign in through the provider. Public providers such as gmail.com are refused, because anyone can have one.
- Pick the role a new member gets the first time your provider sends them.
- Save. Pushify contacts the provider before storing anything, so a wrong issuer is refused here rather than by the first person who tries to sign in.
- Sign out and enter an address in one of those domains on the login page — the password field is replaced with a single button.
Requiring SSO#
With "Require single sign-on" on, passwords, GitHub and Google all stop working for those domains. That is the point of it: disabling someone in your identity provider is then enough to lock them out of Pushify. Two-factor authentication still applies on top — SSO says who someone is, it does not waive a second factor you asked for.
Test it before you require it
When it does not work#
- The provider says the redirect URI does not match
- Copy it again from Settings → Single sign-on. It is derived from your API address, so it changes if that does.
- "is not verified with the identity provider"
- The provider sent an address it has not verified. In Entra, add the email optional claim; in Okta, check the user has a verified primary email.
- "is not in a domain this connection signs in"
- The address is real but its domain is not on your list. Add it, or have the person use their work address. Sub-domains do not count: @eu.acme.com is not @acme.com.
- The sign-in could not be verified
- The token failed signature or claim checks. Usually the client secret is wrong or expired — Entra secrets expire, often after six months.
- Everyone is locked out
- On the server, delete the row from sso_connections for your organization; password login works again immediately.
Error handling#
Standard HTTP status codes, with a JSON body that says what went wrong.
HTTP status codes#
| Code | Description |
|---|---|
| 200 | Success |
| 201 | Created — the resource was created |
| 400 | Bad request — invalid parameters |
| 401 | Unauthorized — invalid or missing API key |
| 403 | Forbidden — insufficient permissions or scope |
| 404 | Not found — the resource does not exist |
| 429 | Too many requests — rate limit exceeded |
| 500 | Internal server error |
Error response#
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or expired API key"
}
}Common error codes#
UNAUTHORIZED- API key is missing, invalid, or expired
INSUFFICIENT_SCOPE- API key lacks required permissions
NOT_FOUND- Requested resource was not found
VALIDATION_ERROR- Request body or parameters are invalid
RATE_LIMITED- Too many requests, slow down
CONFLICT- Resource already exists or state conflict
Rate limits#
Limits apply per API key and depend on the plan.
| Plan | Rate limit |
|---|---|
| Free | 60 requests/min |
| Hobby | 120 requests/min |
| Pro | 300 requests/min |
| Business | 600 requests/min |
| Enterprise | Unlimited |
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.
Handling errors in code#
async function apiRequest(endpoint, options = {}) {
const response = await fetch('https://api.pushify.dev/api/v1' + endpoint, {
...options,
headers: {
'Authorization': 'Bearer ' + process.env.PUSHIFY_API_KEY,
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(error.message || 'API request failed');
}
return response.json();
}