Make & Automation API
Connect Velocity to Make, n8n or your own scripts with the REST API behind the Zapier app.
Overview
The automation API is a small REST API made for no-code platforms. It serves the same triggers, actions and searches as the Zapier app (listed in Zapier as Velocity Quest). It's available on every plan under two base paths that behave identically:
https://velocity.quest/api/make/…, for Make and other HTTP-based toolshttps://velocity.quest/api/zapier/…, which the Zapier app uses
Velocity has no packaged Make app. In Make, use the HTTP module for actions, searches and polling, and a Webhooks custom webhook for instant triggers. This page uses /api/make throughout.
Writes go through the same code as edits in the app. They send Slack notifications, fire webhooks and run your automations, and a status change syncs to a linked GitHub or Sentry issue.
Authentication
Use a personal API key (velu_ prefix) from Settings → Account → Personal API Keys. Send it as a bearer token or in X-API-Key:
Authorization: Bearer velu_your_personal_key
# or
X-API-Key: velu_your_personal_keyWorkspace keys (vel_) are not accepted here. A personal key acts as you, in any workspace you belong to, so every call must name the workspace with workspaceId: in the query string for GET, in the JSON body for POST. Personal key scopes aren't checked on these endpoints.
The Zapier app signs in with OAuth instead. Its token (vel_at_) is tied to the one workspace chosen at sign-in, so it can leave workspaceId out, and naming another workspace is refused. It needs automation:read for reads and automation:write for actions and hook subscriptions.
Find your workspace IDs with GET /api/make/auth/me. It's also a good connection test:
{
"user": { "id": "…", "email": "ada@acme.com", "display_name": "Ada" },
"workspaces": [{ "id": "…", "slug": "acme", "name": "Acme" }]
}Errors
| Status | Body | Meaning |
|---|---|---|
| 401 | invalid_api_key, invalid_key_prefix, missing_api_key | No key, an unknown key, or not a velu_ key |
| 401 | invalid_token | OAuth token expired or revoked: refresh it |
| 403 | insufficient_scope | OAuth token lacks the scope named in scope |
| 403 | forbidden | Not a member of the workspace, or not the token's workspace |
| 400 | missing_workspaceId | API key call without workspaceId |
| 400 | {"error":"<field>: <message>"} | Validation failure |
| 400 | invalid_reference | A referenced ID (in field) isn't in the workspace, or a status or cycle belongs to another team |
| 404 | issue_not_found | No such issue in the workspace |
| 429 | Over 300 requests a minute per user, across all their keys and Zaps. Honor Retry-After. |
Conventions
- Priority is returned as a number:
0urgent,1high,2medium,3low,4none. Actions take the namesURGENT,HIGH,MEDIUM,LOW,NONE, in any case. - Dates are
YYYY-MM-DD. An ISO timestamp is accepted, and its date part is used. - Issue references accept an issue's UUID or its identifier (
ENG-12, any case). An identifier two issues share is treated as not found: use the UUID. - Lists such as
labelIdsare a JSON array or a comma-separated string.
Blank vs. null
In action bodies, an empty string (or only whitespace) means “not given”, and the field is left alone. No-code tools send "" for every field you didn't map. null means clear the field.
Blank entries in a list are dropped, and a list with nothing left counts as not given. An unmapped labels field therefore never removes labels. To remove every label, send "labelIds": null.
// Changes the title, clears the due date, leaves everything else alone.
{ "workspaceId": "…", "issueId": "ENG-12",
"title": "Fix login on Safari", "dueDate": null, "assigneeId": "", "labelIds": "" }Triggers
Each trigger can be polled, or delivered instantly through a hook. Both give the same record. Polls return newest first. limit defaults to 50, with a maximum of 100. Deduplicate on id.
| Trigger | Poll | Hook event |
|---|---|---|
| New issue | GET /triggers/issues | issue.created |
| Updated issue | GET /triggers/issues/updated | issue.updated |
| Issue status changed | GET /triggers/issues/status-changed | issue.status_changed |
| New comment | GET /triggers/comments | comment.created |
| New project | GET /triggers/projects | project.created |
| Updated project | GET /triggers/projects/updated | project.updated |
| Cycle started | GET /triggers/cycles/started | cycle.started |
| Cycle completed | GET /triggers/cycles/completed | cycle.completed |
Issue and cycle polls also take teamId (a UUID). A poll record is the issue as it is now. The updated-issue poll gives one record per save that changed the status, priority, assignee, title, due date, estimate, team, project or cycle. The status-changed poll adds from_status, to_status, actor_id and changed_at. The updated-project poll skips projects that haven't been saved since they were created.
Hooks
To receive events as they happen, subscribe a URL, such as a Make custom webhook, to one event:
POST /api/make/hooks
{ "workspaceId": "…", "targetUrl": "https://hook.make.com/…", "event": "issue.status_changed" }
→ 201 { "id": "<hook id>" }
DELETE /api/make/hooks/<hook id>
→ 204Events: issue.created, issue.updated, issue.status_changed, issue.deleted, comment.created, project.created, project.updated, cycle.started, cycle.completed. Deliveries use the webhook payload format, with the record under data.
- The target must be a public
http(s)URL:400 invalid_target_urlotherwise, and503 target_url_unresolvableif its hostname doesn't resolve. - A hook belongs to whoever subscribed it. Only they, or a workspace admin or owner, can delete it. It is removed when they leave the workspace.
- If the target answers
410 Gone, the hook is deleted. That's how to unsubscribe from the receiving side. - Admins see these hooks in Settings → Webhooks, alongside the ones created there.
- Hooks subscribed here have no signing secret. Their deliveries carry an empty
X-Velocity-Signatureheader. Keep the target URL private.
Actions
All actions are POST with a JSON body. Every referenced ID is checked against the workspace, and a status or cycle against the issue's team.
POST /actions/create-issue
| Field | Type | Notes |
|---|---|---|
teamId | uuid | Required |
title | string | Required, up to 500 characters |
description | string | Plain text. Blank lines separate paragraphs. |
priority | URGENT…NONE | Default NONE |
statusId | uuid | A status of the team. Defaults to the team's default status. |
assigneeId | uuid | A workspace member |
projectId, cycleId | uuid | The cycle must be the team's |
parentId | issue ref | |
labelIds | uuid list | Workspace labels |
dueDate | date | |
estimate | integer 0–100 |
POST /actions/update-issue
issueId (an issue ref, required) plus any of title, description, priority, statusId, assigneeId, projectId, cycleId, parentId, dueDate and estimate. Send null to clear one. For labels, send either labelIds to replace them all, or addLabelIds and removeLabelIds. Only the fields you give change. A request that gives no fields is 400.
Both issue actions return the issue with its description, a url to it in Velocity, and label_ids.
POST /actions/create-comment
{ "workspaceId": "…", "issueId": "ENG-12", "body": "Deployed to staging",
"parentId": "<comment uuid, optional: a reply to a comment on the same issue>" }Returns the comment with its issue and an issue_url.
POST /actions/create-project
name (required), identifier (up to 10 letters and digits; derived from the name when omitted), description, status (backlog, planned, in_progress, paused, completed, cancelled; default backlog), leadId, startDate, targetDate. An identifier that's already used is 409 identifier_taken.
Searches
All GET. Each returns an array, which is empty when nothing matches.
| Endpoint | Parameters | Returns |
|---|---|---|
/searches/find-issue | identifier or id (exact), or query (title, identifier, description) | Issues, up to 25, most recently updated first |
/searches/find-user | email (case-insensitive) | Workspace members |
/searches/find-project | name (name or identifier contains) | Projects, exact matches first, up to 25 |
Options
GET endpoints for dropdowns, each returning [{ id, name, … }]: /options/teams, /options/statuses, /options/members, /options/labels, /options/projects and /options/cycles. Statuses, labels and cycles take an optional teamId.