Repository navigation
Billing and Pricing
billing and pricing are separate packages, danny.vn/vngcloud/billing
and danny.vn/vngcloud/pricing. They are not clients on the root client from
Services; each has its own New(cfg).
Budget, cost, balance, and price amounts are VND. Resource Cost has no
established meaning or currency.
Budget, cost, and balance calls (the billing package) are per account: they
send no project ID and ignore the region in Config. Price quotes (the
pricing package) use the region, because they call a regional gateway. A
Config always needs a region, since the rest of the SDK needs one.
package main
import (
"context"
"log"
"danny.vn/vngcloud"
"danny.vn/vngcloud/billing"
"danny.vn/vngcloud/pricing"
)
func main() {
ctx := context.Background()
cfg, err := vngcloud.NewConfig(
vngcloud.WithRegion("hcm-3"),
vngcloud.WithIAMUser(&vngcloud.IAMUserAuth{
RootEmail: "<root-email>",
Username: "<iam-username>",
Password: "<password>",
}),
)
if err != nil {
log.Fatal(err)
}
client := billing.New(cfg)
priceClient := pricing.New(cfg)
_ = ctx
_ = client
_ = priceClient
}The rest of this page assumes cfg and ctx from this setup, plus
client := billing.New(cfg).
A budget caps spend for one period type, MONTHLY or QUARTERLY, and one
budget type, ACTUAL or FORECASTED. The account can hold at most one
budget per type; the server rejects a second CreateBudget of a type it
already has.
Create a budget, then pause it with UpdateBudget. Pausing sends only the
Status field, so build it with vngcloud.Ptr:
created, err := client.CreateBudget(ctx, &billing.CreateBudgetInput{
Name: "monthly-actual",
PeriodType: billing.PeriodMonthly,
Type: billing.TypeActual,
LimitAmount: 5_000_000,
})
if err != nil {
log.Fatal(err)
}
if _, err := client.UpdateBudget(ctx, &billing.UpdateBudgetInput{
BudgetUUID: created.Budget.UUID,
Status: vngcloud.Ptr(billing.StatusPaused),
}); err != nil {
log.Fatal(err)
}UpdateBudget sends only the fields you set; every other field of the
budget stays as it was. There is no separate pause operation.
UpdateBudget and UpdateBudgetThreshold need at least one field set;
calling either with none is an error before any request goes out.
CreateBudget and CreateBudgetThreshold return the new object's UUID; a
create response with no UUID is an error too, rather than a budget or
threshold the SDK cannot name.
A threshold triggers an alert once a budget's spend crosses a percentage.
The server starts a new threshold enabled, whatever the create request
sends; disable it with UpdateBudgetThreshold:
threshold, err := client.CreateBudgetThreshold(ctx, &billing.CreateBudgetThresholdInput{
BudgetUUID: created.Budget.UUID,
ThresholdType: billing.TypeActual,
ThresholdPercentage: 80,
})
if err != nil {
log.Fatal(err)
}
if _, err := client.UpdateBudgetThreshold(ctx, &billing.UpdateBudgetThresholdInput{
BudgetUUID: created.Budget.UUID,
ThresholdUUID: threshold.Threshold.UUID,
Enabled: vngcloud.Ptr(false),
}); err != nil {
log.Fatal(err)
}DeleteBudget and DeleteBudgetThreshold need nothing beyond the UUID; the
SDK adds no extra step. Deleting a budget deletes its thresholds on the
server, so the SDK does not delete them first:
if _, err := client.DeleteBudget(ctx, &billing.DeleteBudgetInput{
BudgetUUID: created.Budget.UUID,
}); err != nil {
log.Fatal(err)
}GetCostOverview, ListCostResources, and ListBudgetAlerts decode the
console's own field names, but no live capture has yet shown a response
with real cost or alert data for these three. Until one does, treat a field
that reads as zero as unconfirmed rather than as a true zero.
GetCurrentPeriodCost reports the account's current billing period:
period, err := client.GetCurrentPeriodCost(ctx, nil)
if err != nil {
log.Fatal(err)
}
log.Printf("actual cost this period: %.0f", period.PeriodCost.ActualCost)ListCostResources returns at most 200 rows per page; a size above 200 is
capped to 200. Loop on Page up to TotalPage to read every row:
var resources []billing.CostResource
for page := 1; ; page++ {
out, err := client.ListCostResources(ctx, &billing.ListCostResourcesInput{
StartDate: "2026-09-01",
EndDate: "2026-09-30",
Page: page,
})
if err != nil {
log.Fatal(err)
}
resources = append(resources, out.Items...)
if page >= out.TotalPage {
break
}
}GetBalances reads the account's cash and POC (pay-on-credit) balances.
Each field is nil when the account has none of that kind:
balances, err := client.GetBalances(ctx, nil)
if err != nil {
log.Fatal(err)
}
if balances.Balances.Cash != nil {
log.Printf("cash: %.0f", *balances.Balances.Cash)
}ListResources(ctx, nil) reads the unfiltered central resource list across
products. It takes no filters or pagination parameters and sends no region
or project headers. Items contains resource rows; nullable counts and
Extra preserve the server summaries and raw warning/alarm thresholds.
Counts do not establish completeness. A row can outlive its resource. Two
deleted vMonitor log projects remained listed as active in the observed account.
Confirm that a resource exists with its product's own read.
resources, err := client.ListResources(ctx, nil)
if err != nil {
log.Fatal(err)
}
log.Println(len(resources.Items))Resource retains artifact identity, product, billing type, renewal type,
nullable channel, renewal period, dates, and IsRenewing. Times are epoch
milliseconds. RenewTypeNonRenewable, RenewTypeManual, and
RenewTypeAutoRenew name the known strings; unknown strings remain visible.
Status fields, billing element quantity, and metadata preserve raw JSON.
Creator tags are omitted from the public model and routine captures.
Missing, null, or malformed envelope/list structure returns
*vngcloud.APIError. An empty array is valid. Cost is never a price or
estimated charge. The list does not quote every resource. For a vStorage
project's joined state and price, use
Storage: Projects.
Prices are VND totals that include value-added tax (VAT); the API gives no VAT breakdown or rate. GreenNode's invoice documentation states that prepaid unit prices include VAT when an action is performed on a resource.
pricing.GetQuote asks what a resource would cost to create or resize. It
changes nothing and places no order:
quote, err := priceClient.GetQuote(ctx, &pricing.GetQuoteInput{
ResourceType: pricing.ResourceSnapshot,
})
if err != nil {
log.Fatal(err)
}
log.Printf("optimum price: %.0f", quote.OptimumPrice)ResourceInfo is a map[string]any describing the resource in the create
call's own shape; leave it nil for a resource type that needs none. Action
is pricing.ActionCreate or pricing.ActionResize; empty sends
ActionCreate, so code written before Action existed is unchanged.
pricing.ResourceSnapshot, pricing.ResourcePublicVIP,
pricing.ResourceServer, pricing.ResourceVolume, and
pricing.ResourceLoadBalancer are the verified resource types. Other types
work through the plain string.
A paid write in the SDK, such as monitor.CreateLogProject, has its own
Quote... method that takes the write's own Input and returns
*pricing.GetQuoteOutput, so the quote always prices the exact resource the
write would create:
quote, err := computeClient.QuoteCreateServer(ctx, &compute.CreateServerInput{
ZoneID: "<zone-id>", FlavorID: "<flavor-id>", ImageID: "<image-id>",
RootDiskSize: 20, RootDiskTypeID: "<volume-type-id>",
})
if err != nil {
log.Fatal(err)
}
log.Printf("server would cost %.0f VND a month", quote.OptimumPrice)volumeClient.QuoteCreateVolume and lbClient.QuoteCreateLoadBalancer work
the same way for a volume and a load balancer create. A create quote
requires and sends only the fields that change the price, as the console
does:
| Quote | Required | Optional, never sent |
|---|---|---|
QuoteCreateServer |
ZoneID, FlavorID, ImageID, RootDiskSize, RootDiskTypeID; the data disk pair when set |
Name, VPCID, SubnetID, SecurityGroupIDs, SSHKeyID
|
QuoteCreateVolume |
ZoneID, Size, VolumeTypeID
|
Name |
QuoteCreateLoadBalancer |
PackageID, ZoneID
|
Name, Scheme, SubnetID, Type
|
A quote checks the shape of the IDs and of Scheme when they are set, so
a bad value fails there as it will at the create. It does not check Name
or Type, which it never sends.
Encryption prices differently for the two resources. The server quote does not
send the disk encryption type IDs, only encryptionVolume; the volume quote
sends EncryptionTypeID when set. A server with an encrypted disk adds a CES
line of 30% of the flavor price (85,140 VND on s2-general-1x2), so the quote
and the guard price it through encryptionVolume. An encrypted volume costs the
same as a plain one (32,000 VND for 10 GB). See
Compute-Servers and
Volume.
The create still requires every field. The create's price guard quotes the
same priced-only body, so a quote and the guard price one request. Both
ignore the Input's MaxPrice and NoWait fields, which govern only the
write itself once it orders something, and neither sends user data.
computeClient.QuoteResizeServer and volumeClient.QuoteResizeVolume price
a flavor change or a grow the same way, from compute.ResizeServerInput
and volume.ResizeVolumeInput. A resize quote is the new configuration's
price for the rest of the current period, prorated to the minute, not the
difference from the old one. QuoteResizeVolume reads the volume fresh
on every call to learn its current size and type, independently of
ResizeVolume's own read, the same way QuoteCreateLogProject always
rereads its own class list rather than sharing a read with its create.
lbClient.QuoteResizeLoadBalancer prices a change to a resource that
already exists, rather than a create: it takes a
loadbalancer.ResizeLoadBalancerInput (LoadBalancerID and the new
PackageID) and sends Action: pricing.ActionResize instead of
ActionCreate. A resize quote for an unknown LoadBalancerID returns the
server's own error unchanged, since the price guard checks input shape, not
that the resource exists.
A paid write refuses to order above its own MaxPrice (VND a month, default
0), with an error wrapping vngcloud.ErrPriceAboveMax. The load balancer
writes also refuse a create quote of 0 or less, or a resize quote of
exactly 0 (a negative resize quote is a refund and passes), whatever
MaxPrice is, with an error wrapping vngcloud.ErrUnpriced: nothing in
vLB is free, so such a quote means the gateway could not price the input.
if _, err := monitorClient.CreateLogProject(ctx, in); errors.Is(err, vngcloud.ErrPriceAboveMax) {
log.Fatal("quote is above the price you approved")
}monitor.ErrPriceAboveMax is the same value as vngcloud.ErrPriceAboveMax,
so code written against either name still works.
vServer has no hourly rate: a quote is VND a month, and a prepaid account
pays one month at once from its credit wallet when a server or volume is
created. A delete refunds the unused value, counted to the minute. Prices
are public list prices and can change; a Quote... call always returns the
account's current price, so treat this table as a rough guide, not a
guarantee:
| Item | Monthly (VND) |
|---|---|
| Server, 1 vCPU, 2 GB, 20 GB SSD root | ~347,800 |
| Server, 2 vCPU, 4 GB, 20 GB SSD root | ~631,600 |
| SSD volume | ~3,200 a GB |
| Volume, 10 GB SSD | ~32,000 |
Every paid create or resize in compute and volume refuses to send a
write priced above its own MaxPrice, which defaults to 0: setting
MaxPrice is the caller's explicit consent to pay up to that amount, the
role the CLI's --max-price flag plays for the matching command. A quote
of 0 is refused with vngcloud.ErrUnpriced: nothing in vServer is free, so
the gateway could not price the input. A
destructive write, such as deleting a server or a volume, needs no price
consent, since it does not order anything; the CLI instead requires its
--yes flag there, since a delete cannot be undone.
If a server or volume is managed by OpenTofu or Terraform, a write made through the SDK drifts from that state; keep such a resource's writes in its own tool.
pricing.DiscoveryPrice keeps unknown prices and discounts as nil. A quoted
price belongs to the account and uses VND with tax as returned. Discovery
metadata records the region, logical request count, completion coverage, safe
failures, and pricing notes. QuotedAt is UTC completion time, not an atomic
snapshot. Unread excluded catalogs have a nil ExcludedGroups count.
pricing.PricingNote distinguishes published, observed, conflicting, and
unverified evidence. pricing.DiscoveryNotes() returns reviewed source notes
without fetching websites. Published programs do not establish account
eligibility. Observed term comparisons do not promise universal discounts.
Effective storage rates apply at the quoted size. Unknown RAM units, bandwidth,
egress, and other excluded charges remain unknown.
Partial discovery returns *pricing.DiscoveryError with the validated output.
Use errors.Is(err, pricing.ErrDiscoveryIncomplete) to detect incomplete
coverage. errors.As still finds the original *vngcloud.APIError, and
cancellation remains visible through errors.Is. A failure before any usable
row returns the original error without partial output.
SDK
- Getting Started
- Authentication
- Configuration
- Errors
- Services
- Backup
- Billing and Pricing
- CDN
- Compute
- Compute Servers
- DNS
- Container Registry
- IAM
- Load Balancer
- Monitor
- Monitor Alerts
- Monitor Log Alarms
- Network
- Network Route Tables
- Network ACLs
- Network DHCP Options
- Network Virtual IPs
- Tagging
- Storage
- Storage Workload Access
- Storage Objects
- Storage Object Copy
- Storage: Projects
- Storage: Bucket Policy
- Storage: Bucket Settings
- Security
- Volume
- VKS
- Limitations
CLI