Pular para o conteúdo

Buscar na documentação

Buscar na documentação do Skiffly

API pública

One GraphQL endpoint for everything the dashboard, the CLI and the MCP server do. Token types, rate limits, common operations and Railway compatibility.

POST https://api.skiffly.dev/graphql        (alias: /graphql/v2)
Authorization: Bearer <token>
Content-Type: application/json

GraphiQL is available at the same URL in a browser. The schema follows the Railway Public API: Relay connections, input objects, DeploymentStatus in upper case, variables as a map, so documents written for Railway usually run unchanged.

Tokens#

TokenHeaderScopeCreate
AccountAuthorization: Bearer <session token>every workspace of the userskiffly login stores one
WorkspaceAuthorization: Bearer skiffly_…one workspace; read or write scopeSettings → Developer → Create token, or apiTokenCreate(input: { name, workspaceId, scopes })
ProjectProject-Access-Token: skiffly_project_…one environment of one project: deploy, variables, logs, domains, volumesprojectTokenCreate(input: { projectId, environmentId, name })

Tokens are shown once. Project tokens are the right choice for CI: they cannot see other projects or billing. Anything outside the token's scope returns FORBIDDEN.

Rate limits per workspace plan: Free 100 requests/hour; Hobby 1 000/hour and 10/second; Pro 10 000/hour and 50/second. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset; a 429 includes Retry-After.

A shell helper#

export SKIFFLY_TOKEN=skiffly_...
gql() { curl -s https://api.skiffly.dev/graphql -H "Authorization: Bearer $SKIFFLY_TOKEN" -H 'content-type: application/json' \
  --data-binary "$(node -e 'const [q,v]=process.argv.slice(1);process.stdout.write(JSON.stringify({query:q,variables:JSON.parse(v||"{}")}))' "$1" "$2")"; echo; }
 
gql '{ me { login workspaces { id name role } } }'

The CLI does the same with skiffly api '<query>' -v '<json>' using its saved token.

Walkthrough: project → service → domain#

gql 'mutation($ws:String!){ projectCreate(input:{workspaceId:$ws, name:"wevo"}){ id environments { edges { node { id name } } } } }' '{"ws":"<workspaceId>"}'
 
# repository (Railpack, or the Dockerfile in the root)
gql 'mutation{ serviceCreate(input:{projectId:"<projectId>", name:"web", source:{repo:"acme/web"}, branch:"main"}){ id } }'
# image, with variables
gql 'mutation{ serviceCreate(input:{projectId:"<projectId>", name:"redis", source:{image:"redis:7"}, port:6379, variables:{ REDIS_ARGS:"--save 60 1" }}){ id } }'
 
# variables (serviceId null = shared); values may reference other services
gql 'mutation{ variableCollectionUpsert(input:{projectId:"<projectId>", environmentId:"<envId>", serviceId:"<serviceId>", variables:{ REDIS_URL:"${{redis.REDIS_URL}}" }}) }'
gql '{ variables(projectId:"<projectId>", environmentId:"<envId>", serviceId:"<serviceId>") }'
 
# domains
gql 'mutation{ serviceDomainCreate(input:{serviceId:"<serviceId>", environmentId:"<envId>"}){ domain } }'
gql 'mutation{ customDomainCreate(input:{projectId:"<projectId>", serviceId:"<serviceId>", environmentId:"<envId>", domain:"www.example.com"}){ id status { dnsRecords { recordType fqdn requiredValue } } } }'
gql 'mutation{ customDomainVerify(id:"<domainId>"){ status { verified } } }'
 
# volume, settings, deploy, logs
gql 'mutation{ volumeCreate(input:{projectId:"<projectId>", serviceId:"<serviceId>", environmentId:"<envId>", mountPath:"/data", sizeGb:5}){ id } }'
gql 'mutation{ serviceInstanceUpdate(serviceId:"<serviceId>", environmentId:"<envId>", input:{ memoryBytes:1073741824, cpuMillis:1000, healthcheckPath:"/healthz", numReplicas:1 }) }'
gql 'mutation{ serviceInstanceDeployV2(serviceId:"<serviceId>", environmentId:"<envId>") }'
gql '{ deployments(input:{serviceId:"<serviceId>", environmentId:"<envId>"}, first:3){ edges { node { id status message createdAt } } } }'
gql '{ deploymentLogs(deploymentId:"<deploymentId>", limit:200){ timestamp severity message } }'

serviceInstanceDeployV2 returns the deployment id; poll deployment(id) { status message } until SUCCESS or FAILED.

Errors#

GraphQL errors carry extensions.code: UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, BAD_USER_INPUT, CONFLICT, PLAN_LIMIT (with the limit that was hit), TEMPLATE_VARIABLES_REQUIRED (with extensions.missing), RATE_LIMITED.

Railway compatibility#

Same names and shapes where the semantics match: project, projects, environment(s), service, serviceInstance, deployment(s), deploymentLogs, environmentLogs, variables, domains, customDomain*, serviceDomain*, volume*, projectToken*, apiToken*, templates, template, templateDeployV2, serviceInstanceUpdate, serviceInstanceDeployV2, serviceInstanceRedeploy, variableUpsert, variableCollectionUpsert, variableDelete, deploymentCancel, deploymentRedeploy, deploymentRestart, deploymentRollback, tcpProxy*. Arguments are ID! but documents declaring $id: String! are accepted.

Differences that matter:

RailwaySkiffly
buildLogs and deploymentLogsone deploymentLogs; Log.stream is build, runtime or system
deploymentRestart(id)also serviceRestart(serviceId, environmentId)
serviceConnect to attach a sourceserviceUpdate(id, { source })
resources are plan-levelserviceInstanceUpdate takes cpuMillis / memoryBytes
Workspace.plan objectPlan enum FREE / HOBBY / PRO
Subscriptions for logsHTTP only; poll deploymentLogs
Staged changes (environmentStageChanges, environmentPatchCommit*)none: changes apply immediately, redeploy when needed
Regions, egress gateways, private networks, buckets, functions, sandboxes, teams/RBAC, feature flags, webhooksnot available

Skiffly-only additions: billingAccount, ledger, payments, topupCreate, usage, serviceMetrics, nodes, githubInstallations, githubApp, tcpProxies, Service.internalHost, ServiceInstance.branch, ServiceUpdateInput.runtimeClass.

The full SDL is served by introspection at the endpoint; the GraphQL reference lists every root field.