Async Operations
Many GPCN™ operations — creating VMs, attaching volumes, allocating public IPs — are asynchronous. Instead of blocking until complete, the API returns immediately with a job ID that you poll for progress.
How It Works
- Submit the operation — the API returns
202 Acceptedwith a job object - Poll for status — send the job ID to the polling endpoint
- Check completion — the job transitions through statuses until it reaches
completedorfailed
Job Statuses
| Status | Description |
|---|---|
pending |
Job is queued but hasn't started |
in_progress |
Job is actively being processed |
completed |
Job finished successfully. The resourceId field contains the created/modified resource. |
failed |
Job encountered an error. The error field contains details. |
Polling Endpoint
POST /resource/jobs
Poll one or more jobs in a single request (batch polling).
Request Body
{ "jobIds": ["job-uuid-1", "job-uuid-2"] }
1–50 job IDs per request.
Response (200 OK)
{
"success": true,
"data": {
"jobs": [
{
"id": "job-uuid-1",
"status": "completed",
"progress": 100,
"resourceType": "vm",
"resourceId": "550e8400-e29b-41d4-a716-446655440000",
"error": null,
"createdAt": "2026-02-21T10:00:00Z",
"updatedAt": "2026-02-21T10:02:30Z",
"completedAt": "2026-02-21T10:02:30Z"
},
{
"id": "job-uuid-2",
"status": "in_progress",
"progress": 45,
"resourceType": "volume",
"resourceId": null,
"error": null,
"createdAt": "2026-02-21T10:01:00Z",
"updatedAt": "2026-02-21T10:02:30Z",
"completedAt": null
}
]
}
}
Job Response Fields
| Field | Type | Description |
|---|---|---|
id |
UUID | Job identifier |
status |
string | pending, in_progress, completed, or failed |
progress |
integer | 0–100 percentage |
resourceType |
string | vm, volume, network, gpu |
resourceId |
UUID | ID of the created/modified resource (null until completed) |
error |
string | Error message if failed (null otherwise) |
createdAt |
ISO 8601 | When the job was created |
updatedAt |
ISO 8601 | Last status update |
completedAt |
ISO 8601 | When the job finished (null if still running) |
Polling Pattern
Simple polling loop
# Submit the operation
RESPONSE=$(curl -s -X POST "https://api.gpcn.com/v1/resource/virtual-machines" \
-H "Content-Type: application/json" \
-H "X-API-Key: gpcn_your_api_key_here" \
-d '{"name": "web-server", "datacenterId": "dc-uuid", "imageId": 12, "configurationId": 45}')
JOB_ID=$(echo $RESPONSE | jq -r '.data[0].id')
# Poll until complete
while true; do
STATUS=$(curl -s -X POST "https://api.gpcn.com/v1/resource/jobs" \
-H "Content-Type: application/json" \
-H "X-API-Key: gpcn_your_api_key_here" \
-d "{\"jobIds\": [\"$JOB_ID\"]}" | jq -r '.data.jobs[0].status')
echo "Status: $STATUS"
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
break
fi
sleep 3
done
// Submit the operation
const createResponse = await fetch(
"https://api.gpcn.com/v1/resource/virtual-machines",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.GPCN_API_KEY!,
},
body: JSON.stringify({
name: "web-server",
datacenterId: "dc-uuid",
imageId: 12,
configurationId: 45,
}),
}
);
const { data: jobs } = await createResponse.json();
const jobId = jobs[0].id;
// Poll until complete
async function pollJob(jobId: string): Promise<any> {
while (true) {
const pollResponse = await fetch(
"https://api.gpcn.com/v1/resource/jobs",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.GPCN_API_KEY!,
},
body: JSON.stringify({ jobIds: [jobId] }),
}
);
const { data } = await pollResponse.json();
const job = data.jobs[0];
if (job.status === "completed" || job.status === "failed") {
return job;
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}
}
const result = await pollJob(jobId);
if (result.status === "completed") {
console.log("Resource created:", result.resourceId);
} else {
console.error("Job failed:", result.error);
}
import requests, os, time
API = "https://api.gpcn.com/v1"
HEADERS = {"Content-Type": "application/json", "X-API-Key": os.environ["GPCN_API_KEY"]}
# Submit the operation
resp = requests.post(f"{API}/resource/virtual-machines", headers=HEADERS, json={
"name": "web-server", "datacenterId": "dc-uuid", "imageId": 12, "configurationId": 45
})
job_id = resp.json()["data"][0]["id"]
# Poll until complete
while True:
poll = requests.post(f"{API}/resource/jobs", headers=HEADERS, json={"jobIds": [job_id]})
job = poll.json()["data"]["jobs"][0]
print(f"Status: {job['status']}")
if job["status"] in ("completed", "failed"):
break
time.sleep(3)
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
func main() {
api := "https://api.gpcn.com/v1"
apiKey := os.Getenv("GPCN_API_KEY")
// Submit the operation
body, _ := json.Marshal(map[string]any{
"name": "web-server", "datacenterId": "dc-uuid",
"imageId": 12, "configurationId": 45,
})
req, _ := http.NewRequest("POST", api+"/resource/virtual-machines", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", apiKey)
resp, _ := http.DefaultClient.Do(req)
var createResult struct{ Data []struct{ ID string } }
json.NewDecoder(resp.Body).Decode(&createResult)
resp.Body.Close()
jobID := createResult.Data[0].ID
// Poll until complete
for {
pollBody, _ := json.Marshal(map[string]any{"jobIds": []string{jobID}})
req, _ := http.NewRequest("POST", api+"/resource/jobs", bytes.NewReader(pollBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", apiKey)
resp, _ := http.DefaultClient.Do(req)
var pollResult struct {
Data struct{ Jobs []struct{ Status string } }
}
json.NewDecoder(resp.Body).Decode(&pollResult)
resp.Body.Close()
status := pollResult.Data.Jobs[0].Status
fmt.Println("Status:", status)
if status == "completed" || status == "failed" {
break
}
time.Sleep(3 * time.Second)
}
}
using System.Net.Http.Json;
var api = "https://api.gpcn.com/v1";
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key",
Environment.GetEnvironmentVariable("GPCN_API_KEY"));
// Submit the operation
var createResp = await client.PostAsJsonAsync($"{api}/resource/virtual-machines",
new { name = "web-server", datacenterId = "dc-uuid", imageId = 12, configurationId = 45 });
var createJson = await createResp.Content.ReadFromJsonAsync<JsonElement>();
var jobId = createJson.GetProperty("data")[0].GetProperty("id").GetString();
// Poll until complete
while (true) {
var pollResp = await client.PostAsJsonAsync($"{api}/resource/jobs",
new { jobIds = new[] { jobId } });
var pollJson = await pollResp.Content.ReadFromJsonAsync<JsonElement>();
var status = pollJson.GetProperty("data").GetProperty("jobs")[0]
.GetProperty("status").GetString();
Console.WriteLine($"Status: {status}");
if (status is "completed" or "failed") break;
await Task.Delay(3000);
}
Recommendations
- Poll interval: Start at 2–3 seconds. For long-running operations (VM creation), you can increase to 5–10 seconds after the first few polls.
- Timeout: Set a reasonable timeout (e.g., 5 minutes for VM creation). If the job hasn't completed, surface an error to the user.
- Batch polling: If you've submitted multiple jobs (e.g., bulk VM creation), poll all job IDs in a single request instead of one at a time.
- Progress tracking: Use the
progressfield (0–100) to display a progress bar in your UI.
Operations That Return Jobs
| Resource | Operations |
|---|---|
| Virtual Machines | Create, delete, resize, add/remove NIC, allocate/release public IP |
| GPU VMs | Create, delete |
| Volumes | Create, delete, attach, detach, resize |
| Networks | Create |
Operations that return 200 OK (start, stop, reset, rename) are synchronous and do not create jobs.
Troubleshooting
| Issue | Solution |
|---|---|
| Rate-limited while polling | You are polling too frequently. Use a minimum 2–3 second interval between requests, and increase to 5–10 seconds for long-running operations. Batch multiple job IDs into a single /resource/jobs call instead of polling each job separately. |
Job stuck in in_progress |
Set a client-side timeout (e.g., 5 minutes for VM creation, 2 minutes for volume operations). If the job exceeds the timeout, log the job ID and surface an error to the user. GPCN™ jobs that genuinely stall will eventually transition to failed. |
Not handling failed status |
Always check for failed in your polling loop. Read the error field for details — common causes include insufficient quota, invalid configuration, or datacenter capacity limits. Never assume a job will succeed. |
| Missed job completion | If your application crashes or disconnects during polling, persist the job ID and resume polling on restart. Job results remain queryable for at least 24 hours after completion. Use the completedAt field to confirm when the job finished. |
| Polling returns empty jobs array | Verify the job ID is correct and that you are authenticated with the same account that created the job. Job IDs from other tenants are not visible to your API key. |
resourceId is null on completed job |
Some delete operations complete without a resourceId because the resource no longer exists. Check the resourceType field and the operation context to determine expected behavior. |
Next Steps
- Virtual Machine Endpoints → — create, delete, and resize VMs (all async)
- Storage Endpoints → — volume operations (all async)
- GPU Endpoints → — GPU VM operations (all async)
- Rate Limiting — manage polling within rate limits
.png)