# Requests and responses

Asynchronous changes, idempotency, pagination, filtering, rate limits and the shape of every response.

Source: https://docs.pantechdynamics.com/api/conventions

## Requests

- **Base URL:** every path in this reference starts here.

  ```http
  https://api.pantechdynamics.com/public/v1
  ```

- **Bodies are JSON.** Send `Content-Type: application/json`. Unknown fields are rejected with `422` and the field's name, so a typo is caught instead of ignored.
- **Ids** look like `vm_06gk3x9tq7m2v8r4y1c5d0zp8w`. The prefix says what it is: `vm_` instance, `ord_` order, `op_` operation, `net_` VPC, `snet_` subnet, `aclr_` firewall rule, `pip_` public IP, `pfr_` port forwarding rule, `sg_` security group, `vol_` volume, `snap_` snapshot, `sshk_` SSH key, `sshg_` SSH access grant.
- **Times** are RFC 3339 in UTC: `2026-10-03T09:30:00Z`.
- **Money** is an integer in minor units with its currency: `{ "amount_minor": 1059200, "currency": "NGN" }` is ₦10,592.00. USD amounts are cents.

## Responses

- **One resource** comes back as the object itself.
- **A list** comes back in an envelope:

  ```json
  { "data": [ … ], "next_cursor": "eyJhdCI6…" }
  ```

- **No value is `null`,** never an empty string or a zero that could be mistaken for a real one. A field is always present.
- **Every response** carries `X-Request-Id`. Quote it if you contact support.

## Asynchronous changes

Creating, changing and deleting infrastructure takes time, so writes do not wait for it. A write returns `202 Accepted` with the operation doing the work:

```json
{
  "operation_id": "op_06gk3x9tq7m2v8r4y1c5d0zp8w",
  "resource_id": "vol_06gk3x9tq7m2v8r4y1c5d0zp8w",
  "status": "submitting"
}
```

The `Location` header points at it. Poll `GET /operations/{operation_id}` every few seconds until `status` is `succeeded` or `failed`, then read the resource. Or open `GET /operations/{operation_id}/events`: a Server-Sent Events stream that sends the operation each time it changes and closes when it finishes.

```sh
# Wait for an operation to finish. Needs jq.
while :; do
  status=$(curl -s "https://api.pantechdynamics.com/public/v1/operations/$OP" \
    -H "Authorization: Bearer $PANTECH_API_KEY" | jq -r .status)
  case "$status" in succeeded|failed) break ;; esac
  sleep 3
done
echo "$status"
```

**A new instance is different.** It is paid for before it is built, so `POST /instances` returns an [order](https://docs.pantechdynamics.com/api/instance-orders). Poll the order until its `status` is `provisioned`; the instance is then ready.

A resource also reports its own progress: `desired_state` is what you asked for, `observed_state` what the platform last saw. When they match, the change has landed.

## Idempotency

Every `POST`, `PUT` and `DELETE` needs an `Idempotency-Key` header: a unique value per change, such as a UUID.

```http
Idempotency-Key: 5f1c2a34-8d4e-4b8e-9a51-0e2f6f7b9c10
```

If a request fails on the network, or you are not sure it arrived, **send it again with the same key**. You get the original result, and the change is never made twice, so you will not create two instances or pay twice. Using the same key for a different request returns `409 IDEMPOTENCY_CONFLICT`.

Make a new key for each new change, and keep it until that change is done.

:::note[Ten exceptions]
These do not replay a retry the usual way:

- `POST /ssh-keys`: if generating a key pair fails on the network, list your SSH keys before trying again. A second attempt with the same name returns `409`, and a generated private key is only ever shown once.
- `DELETE /ssh-keys/{ssh_key_id}`: once the key is gone, a repeat returns `404`. Treat that as done.
- `POST /instances/{instance_id}/console`: each call opens a new console URL. Retrying is safe.
- `POST /instances/{instance_id}/volume-key-credential`: each call issues a new credential, shown once, and ends the previous one for that instance. If a response is lost, call again and use the new credential.
- `POST /webhooks`: a retry with the same key returns the webhook the first request made, but with `secret: null`: the secret is only ever shown once. If you lost it, rotate it.
- `PATCH /webhooks/{webhook_id}`: not replayed, but applying the same change twice leaves the webhook the same.
- `DELETE /webhooks/{webhook_id}`: once the webhook is gone, a repeat returns `404`. Treat that as done.
- `POST /webhooks/{webhook_id}/test`: each call sends another test event.
- `POST /webhooks/{webhook_id}/rotate-secret`: each call rotates again, and only the latest secret and the one before it sign. If a response is lost, rotate again and use the new secret.
- `POST /webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver`: a retry while the delivery is being sent returns `409 WEBHOOK_DELIVERY_IN_PROGRESS`. Treat that as done.
:::

## Pagination

Lists return up to `limit` items (default 50, at most 100) and a `next_cursor`. Pass it back as `cursor` for the next page; it is `null` on the last page.

```console
$ curl "https://api.pantechdynamics.com/public/v1/instances?limit=100&cursor=$NEXT" \
    -H "Authorization: Bearer $PANTECH_API_KEY"
```

A cursor belongs to the filters it was made with. Changing the filters and keeping the cursor returns `400 INVALID_CURSOR`; start again without one.

## Filtering

The lists of instances, VPCs, subnets, security groups, volumes and snapshots take three filters:

- `q`: names containing this text.
- `desired_state`: e.g. `running` or `stopped` for instances, `present` for everything else.
- `observed_state`: e.g. `running`, `stopped`, `failed` for instances, `active` or `failed` for everything else.

```console
$ curl "https://api.pantechdynamics.com/public/v1/instances?observed_state=failed" \
    -H "Authorization: Bearer $PANTECH_API_KEY"
```

## Rate limits

Each key has a budget of requests per minute. Every response to a request with a valid key says where you stand:

| Header | Meaning |
| --- | --- |
| `RateLimit-Limit` | Requests allowed in each window |
| `RateLimit-Remaining` | Requests left in this window |
| `RateLimit-Reset` | Seconds until the window resets |

Past the limit you get `429 RATE_LIMITED` with `Retry-After` in seconds. Wait that long, then retry with the same `Idempotency-Key`. Slow down before you get there: when `RateLimit-Remaining` is low, wait for `RateLimit-Reset`.

## Retries

| Response | Retry? |
| --- | --- |
| Network error, timeout | Yes, with the same `Idempotency-Key` |
| `429` | Yes, after `Retry-After` seconds |
| `503`, `500` | Yes, with backoff (1 s, 2 s, 4 s…), same `Idempotency-Key` |
| `409 CONCURRENT_UPDATE` | Yes, same request and key |
| Any other `4xx` | No, fix the request first |

## Versioning

The version is in the path: `/public/v1`. Within v1, changes only add things: new endpoints, new optional request fields and new response fields. Write clients that ignore fields they do not know. Anything that would break a client comes as `/public/v2`, announced in the [changelog](https://docs.pantechdynamics.com/changelog) with time to move.
