tovin.io

Volume 01 · Reference

Billing reads,explicitly scoped.

tovinio is a read-only spend monitor. The identity you attach to a connection only needs read access to billing APIs and a single identity-validation endpoint — never permissions to create, modify, or delete cloud resources.

Tovin.io connects to AWS, Google Cloud, and DigitalOcean using read-only credentials only. AWS accepts a cross-account IAM role or a long-lived access key with billing read scope; GCP accepts a service account JSON key with billing and BigQuery read; DigitalOcean accepts a read-only personal access token. Every credential is validated on connect, stored under KMS envelope encryption, and rotated by pasting a fresh value — never by granting write access.

Surface area
Three providers · zero mutating calls
Storage
Credentials are KMS-envelope-encrypted at rest
Rotation
Re-paste a fresh credential to rotate; no downtime

Chapter the first

Amazon Web Services

Cross-account IAM role, or a long-lived access key.

The cross-account role flow is preferred: credentials are short-lived, the trust policy is gated by an external ID, and revocation is a single console click in your account. Access keys still work, but rotate them on a schedule and treat the long-lived secret as you would any other production credential.

Deploy the bundled CloudFormation template (or paste the JSON below into a new role) in your account. The trust policy locks the principal to tovinio's ingest role and the ExternalId generated for this connection.

Trust policy · paste from the connection form

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "BILLINGDASH_INGEST_ROLE_ARN"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "EXTERNAL_ID_FROM_TOVIN"
        }
      }
    }
  ]
}

Current minimum permissions

Enough for the present validation and Cost Explorer sync. Tag discovery falls back to Project if ce:GetTags is unavailable; granting it lets tovinio choose the active key automatically.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "TovinCostExplorerRead",
      "Effect": "Allow",
      "Action": [
        "ce:GetCostAndUsage",
        "ce:GetTags"
      ],
      "Resource": "*"
    },
    {
      "Sid": "TovinIdentityCheck",
      "Effect": "Allow",
      "Action": [
        "sts:GetCallerIdentity"
      ],
      "Resource": "*"
    }
  ]
}

Recommended — matches the bundled template

Headroom for tag, dimension, resource, and forecast reads. This is what the CloudFormation template attaches by default.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "TovinCostExplorerRead",
      "Effect": "Allow",
      "Action": [
        "ce:GetCostAndUsage",
        "ce:GetCostAndUsageWithResources",
        "ce:GetTags",
        "ce:GetDimensionValues",
        "ce:GetCostForecast"
      ],
      "Resource": "*"
    },
    {
      "Sid": "TovinIdentityCheck",
      "Effect": "Allow",
      "Action": [
        "sts:GetCallerIdentity"
      ],
      "Resource": "*"
    }
  ]
}

  1. sts:AssumeRole — referenced by the trust policy and gated by the connection's external ID.
  2. sts:GetCallerIdentity — confirms the account ID after the role is assumed.
  3. ce:GetCostAndUsage — daily UnblendedCost, grouped by the active project tag and by service.
  4. ce:GetTags — discovers whether your active cost-allocation key is Project, project, or another spelling configured in your tovinio settings.

On every sync, tovinio asks Cost Explorer which configured project tag key is active, preferring Project then project. Every non-empty tag value becomes a tovinio project with an exact tag rule, if one does not exist already.


Service drill-downs show individual resources — EC2 instance IDs, S3 buckets, RDS clusters — but only after you enable resource-level data in your AWS account. The setting lives exclusively in the AWS console; there is no aws ce, boto3, CloudFormation, or Terraform path for it.

  1. Sign into the AWS console with the management / payer account — a member account cannot toggle this.
  2. Open Billing & Cost Management → Settings.
  3. Scroll to Historical and granular data settings. Under Granular data → Daily granularity, check Resource-level data at daily granularity.
  4. In Selected services, choose All (or a specific subset). At least one service must be selected.
  5. Click Save preferences.
  6. AWS shows "Your data will be enabled within 48 hours". The page locks editing during the propagation window.
  7. After AWS reports the data is enabled, run Sync now from the Connections page in tovinio. The opt-in banner clears once the sync confirms the toggle is on and resource rows have been ingested for the trailing 14 days.
AWS Cost Management settings panel after enabling resource-level data at daily granularity.
Fig. 1Historical and granular data settings after Save. The "Enabling…" notice clears once AWS finishes provisioning.

  • If you use access keys instead of a role, attach the same Cost Explorer + STS read policy to the IAM user. Cross-account roles are preferred — credentials are short-lived and the external ID protects against confused-deputy attacks.
  • Activate the cost-allocation tag in Billing and Cost Management before relying on tag-based project mapping. tovinio auto-checks Project and project first, then creates missing projects from the values it sees.
  • Rotation: regenerate the access key (or update the role's trust policy) and re-paste in the connection form. tovinio re-validates before the new credential is encrypted.

Chapter the second

Google Cloud

Service account JSON key with billing and BigQuery read.

Google Cloud cost ingestion reads the BigQuery billing export table. The Cloud Billing API is used only to validate the service account can see billing accounts; daily spend, project labels, service totals, and resource rows come from BigQuery.

GCP onboarding crosses three resource scopes — service account project, Cloud Billing account, and BigQuery export project. The order below is the one that minimizes back-and-forth and IAM- propagation waits. Read it before clicking, then come back to the relevant chapter for detail.

0.  Pick a "FinOps" Google Cloud project that owns the credential
    and (usually) the billing-export dataset.

1.  CREATE the service account
    Console: IAM & Admin → Service Accounts → Create
    Output:  tovinio-billing@FINOPS_PROJECT.iam.gserviceaccount.com

2.  ENABLE APIs in the FinOps project
    cloudbilling.googleapis.com   (validation calls billingAccounts.list)
    bigquery.googleapis.com       (sync runs queries)

3.  GRANT roles/billing.viewer on the BILLING ACCOUNT
    Console: Billing → <account> → Account management / Permissions
    Add principal: the SA email above.
    NOT project IAM — billing-account IAM.

4.  ENABLE BigQuery billing export (one-time, per Cloud Billing account)
    Billing → Billing export → BigQuery export
    Pick "Standard usage cost". Choose a dataset location you can keep.
    Wait — first rows can take hours, full backfill can take days.

5.  GRANT BigQuery read on the EXPORT project + dataset
    roles/bigquery.jobUser     on the export project
    roles/bigquery.dataViewer  on the export dataset

6.  CREATE a JSON KEY for the service account; download once.
    IAM & Admin → Service Accounts → <SA> → Keys → Add → JSON

7.  PASTE in tovinio: Connections → Google Cloud
    Service account JSON, then the three BigQuery pointer fields.
    See the "Connection-form fields" cheat sheet below.

Total clock time on a fresh project: ~10 minutes of clicking, plus a 24–48h wait before the first BigQuery rows land. Validation in tovinio works as soon as the export table exists; Sync now returns rows once Google has written some.


Create a dedicated service account in a stable billing or FinOps project. Do not reuse an application runtime service account; this identity should exist only to read billing metadata and, later, the billing-export dataset.

  1. Pick the project that will own the credential, usually a separate FinOps, platform, or billing-admin project linked to the same Cloud Billing account.
  2. Console path: IAM & Admin → Service Accounts → Create service account. Name it tovinio-billing or your local equivalent.
  3. Skip broad project roles during creation. The billing role must be granted on the Cloud Billing account, not only on the project that contains the service account.
  4. Enable the Cloud Billing API (cloudbilling.googleapis.com) in the project that contains the service account. Google checks this API before IAM on billingAccounts.list.
  5. Open the service account's Keys tab, create a new JSON key, and paste the complete file into the tovinio connection form.

Equivalent gcloud setup

# Pick a project you use for billing / FinOps administration.
FINOPS_PROJECT_ID="my-finops-project"
SA_ID="tovinio-billing"
SA_EMAIL="${SA_ID}@${FINOPS_PROJECT_ID}.iam.gserviceaccount.com"

gcloud iam service-accounts create "${SA_ID}" \
  --project="${FINOPS_PROJECT_ID}" \
  --display-name="tovinio billing reader" \
  --description="Read-only billing identity for tovinio"

gcloud services enable cloudbilling.googleapis.com \
  --project="${FINOPS_PROJECT_ID}"

gcloud services enable bigquery.googleapis.com \
  --project="${FINOPS_PROJECT_ID}"

gcloud iam service-accounts keys create ./tovinio-gcp-sa.json \
  --iam-account="${SA_EMAIL}" \
  --project="${FINOPS_PROJECT_ID}"

Google Cloud IAM is resource-scoped. The same service account can need one role on the billing account, optional viewer roles on projects, and BigQuery read roles on the export dataset. Grant each role at the narrowest resource that still lets the integration work.

Current minimum roles

roles/billing.viewer          # on the Cloud Billing account
roles/browser                 # on each project you want visible during setup
roles/bigquery.jobUser        # on the billing export project
roles/bigquery.dataViewer     # on the billing export dataset
  1. Billing Account Viewer (roles/billing.viewer) on the Cloud Billing account. This is the role tovinio validates today. It includes billing.accounts.list and billing.accounts.get.
  2. Browser (roles/browser) on each project you want visible for setup and mapping. It exposes project metadata, not resource contents.
  3. BigQuery Job User on the export project and BigQuery Data Viewer on the billing-export dataset let tovinio validate and query the billing export table.

Grant Billing Viewer on the billing account

BILLING_ACCOUNT_ID="000000-000000-000000"
SA_EMAIL="tovinio-billing@my-finops-project.iam.gserviceaccount.com"

gcloud billing accounts add-iam-policy-binding "${BILLING_ACCOUNT_ID}" \
  --member="serviceAccount:${SA_EMAIL}" \
  --role="roles/billing.viewer"

Optional project discovery roles

SA_EMAIL="tovinio-billing@my-finops-project.iam.gserviceaccount.com"

for PROJECT_ID in prod-web prod-data shared-platform; do
  gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
    --member="serviceAccount:${SA_EMAIL}" \
    --role="roles/browser"
done

OAuth scope used by validation

OAuth scopes:  https://www.googleapis.com/auth/cloud-billing.readonly
               https://www.googleapis.com/auth/bigquery
Used at:       Cloud Billing billingAccounts.list
               BigQuery jobs.query / getQueryResults

If you prefer the console, grant the billing account role from the Billing area, not from the project IAM page. Project IAM alone is a common source of failed validation because billingAccounts.list checks billing account permissions.

  1. Open Google Cloud Billing and select the Cloud Billing account you want to connect.
  2. Go to Account management → Permissions or Billing account IAM, depending on your console layout.
  3. Click Add principal, paste the service account email, and assign Billing Account Viewer.
  4. Save, then wait a minute for IAM propagation before pasting the JSON key into tovinio.

You do not need Billing Account Administrator, Project Editor, Owner, Compute Viewer, or Storage Viewer for current validation.


Google Cloud's detailed spend rows live in BigQuery export tables, not in the Cloud Billing API. Enable the export before adding the connection so validation can dry-run a query against the table.

  1. Create or select a billing/FinOps project to hold the export dataset. Keep it linked to the same Cloud Billing account.
  2. Create a BigQuery dataset, commonly billing_export. Use a supported dataset location; US and EU multi-regions are the safest choices when you want current and previous-month backfill.
  3. In Billing, open Billing export → BigQuery export and enable Standard usage cost. Enable Detailed usage cost too if you want the resource fields Google provides.
  4. Copy the actual Project ID that contains the dataset, not the display name shown in Billing. For example, use steel-watch-419, not My Compute Engine Project.
  5. In BigQuery → Explorer, expand that project, then billing_export. Copy the exact table name that starts with gcp_billing_export_v1_. The suffix is generated from the Cloud Billing account, so it is usually not the project ID.
  6. Do not remove Google's billing-export-bigquery@system.gserviceaccount.com dataset access. Google uses that managed identity to write the export tables.
  7. Grant the service account roles/bigquery.jobUser on the export project and roles/bigquery.dataViewer on the dataset.

Heads up — export lag. The BigQuery billing export typically lags the Cloud Billing Console by hours to days. A successful sync that shows zero current-month spend usually just means Google hasn't written today's rows yet — not that the connection is broken. Tovinio surfaces the latest exported usage date on the Connections card so you can tell the two cases apart.

BigQuery reader project grant

# Only needed when the BigQuery billing-export reader is enabled.
EXPORT_PROJECT_ID="my-finops-project"
DATASET_ID="billing_export"
SA_EMAIL="tovinio-billing@my-finops-project.iam.gserviceaccount.com"

gcloud services enable bigquery.googleapis.com \
  --project="${EXPORT_PROJECT_ID}"

gcloud projects add-iam-policy-binding "${EXPORT_PROJECT_ID}" \
  --member="serviceAccount:${SA_EMAIL}" \
  --role="roles/bigquery.jobUser"

bq show --dataset "${EXPORT_PROJECT_ID}:${DATASET_ID}"

BigQuery reader dataset grant

Run from the BigQuery SQL editor as a user who can grant dataset access, or grant the same role from the dataset's Sharing panel.

GRANT `roles/bigquery.dataViewer`
ON SCHEMA `my-finops-project`.billing_export
TO "serviceAccount:tovinio-billing@my-finops-project.iam.gserviceaccount.com";

Find the generated table name

Replace steel-watch-419 and billing_export with your export project ID and dataset. Use the returned gcp_billing_export_v1_... table in the connection form.

SELECT table_name
FROM `steel-watch-419.billing_export.INFORMATION_SCHEMA.TABLES`
WHERE STARTS_WITH(table_name, 'gcp_billing_export_v1_')
ORDER BY table_name;

Quick export sanity query

Run this in BigQuery after the export table appears. Replace the project, dataset, and generated billing-account suffix.

SELECT
  usage_start_time,
  project.id AS project_id,
  service.description AS service,
  sku.description AS sku,
  cost,
  currency,
  labels
FROM `EXPORT_PROJECT_ID.DATASET_ID.gcp_billing_export_v1_XXXXXX_XXXXXX_XXXXXX`
WHERE DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
ORDER BY usage_start_time DESC
LIMIT 20;

Every exported GCP billing row carries the Google Cloud project that incurred the charge. That means a one-GCP-project-per-workload account can map cleanly with account/project rules and no labels. Add labels when a single GCP project hosts multiple tovinio projects.

  • Use the lower-case label key project for new GCP labels. The mapping UI still accepts explicit rules, but consistent labels make the export easier to reason about.
  • Labels are resource-specific. Compute Engine, Cloud Storage, Cloud Run, GKE, Cloud SQL, and BigQuery each have their own UI or CLI path, and some SKUs remain project-only or unlabeled.
  • Expect lag. Labels can take time to appear in billing exports, and historical rows are not rewritten just because you labeled a resource today.

Example labels

# Existing Compute Engine instance.
gcloud compute instances add-labels INSTANCE_NAME \
  --project=PROJECT_ID \
  --zone=us-central1-a \
  --labels=project=acme-prod,env=prod

# Existing Cloud Storage bucket.
gcloud storage buckets update gs://BUCKET_NAME \
  --update-labels=project=acme-prod,env=prod

tovinio's GCP connection form has four inputs. The three BigQuery pointers refer to different resources — easy to swap project ID with dataset name, or to paste the display name where the form expects the ID. Use this cheat sheet while you fill it in.

tovinio form field          Where it comes from
─────────────────────────   ─────────────────────────────────────────────────
Service account JSON        The full JSON file you downloaded in step 6.
                            Paste raw text including private_key. Not a
                            screenshot, not a wrapped secret-manager dict.

Billing export project ID   The Project ID of the project that contains
                            the BigQuery export dataset.
                            Console: BigQuery → Explorer → top-level project.
                            Use the Project ID  (e.g.  steel-watch-419),
                            NOT the display name (e.g. "Acme Finops").

Billing export dataset      The BigQuery dataset name you created/chose
                            in step 4. Conventional name: billing_export.
                            Lower-case, no spaces.

Billing export table        The auto-generated table name that begins with
                            gcp_billing_export_v1_.
                            Console: BigQuery → Explorer → expand dataset →
                            copy the exact table name (one per Cloud
                            Billing account; suffix is the billing account,
                            NOT the project ID).

                            If you can't see the table:
                            • Wait 24–48h after enabling export.
                            • Or run the table-discovery query below.

  1. Open Connections → Google Cloud.
  2. Paste the entire JSON file, including type, project_id, client_email, and private_key.
  3. Enter the BigQuery billing export project, dataset, and table name. Use the export project ID, not the display name. The standard table is copied from BigQuery Explorer and is usually named gcp_billing_export_v1_...; the suffix comes from the Cloud Billing account, not the GCP project ID.
  4. Click Validate & save. tovinio mints an OAuth token using the service account key, calls billingAccounts.list, dry-runs a BigQuery query against the export table, then stores the credential only after validation passes.
  5. The saved connection subject is the service account key's project_id when present, otherwise the service account email.

A successful save confirms both the billing role and BigQuery table access. Run Sync now after the export table has started receiving rows.


  • Malformed JSON — paste the raw key file, not a screenshot, escaped string, or wrapper object from a secret manager.
  • Could not mint a token — the key may be disabled, deleted, copied incompletely, or blocked by an organization policy that restricts service account key usage.
  • Cannot list billing accounts — grant roles/billing.viewer on the Cloud Billing account itself. A project-level Viewer role is not enough.
  • Cloud Billing API has not been used or is disabled — enable cloudbilling.googleapis.com in the project that owns the service account, then retry after propagation.
  • Cannot query the BigQuery export table — verify the table name, enable the BigQuery API on the export project, grant roles/bigquery.jobUser on that project, and grant roles/bigquery.dataViewer on the dataset.
  • Only some projects appear later — add roles/browser on the missing projects or use explicit account/project mapping rules.

  1. To rotate, create a new JSON key for the same service account, paste it into tovinio, and save after validation passes.
  2. Delete the old key from IAM & Admin → Service Accounts → Keys after the replacement is saved.
  3. To revoke completely, remove the billing-account IAM binding and delete or disable all user-managed keys for the service account.
  4. Review user-managed keys periodically with gcloud iam service-accounts keys list and remove keys that are no longer in use.

Workload Identity / federation for tovinio's GCP integration is tracked but not yet implemented. Until then, JSON key rotation is the supported credential lifecycle.

Chapter the third

DigitalOcean

Read-only personal access token.

DO does not have AWS-style cost-allocation tags or a Cost Explorer surface. tovinio reads the same artifact a finance team would — the per-invoice CSV — line by line, then maps each line to a project using the CSV's project_name column (DO's built-in Projects feature) or account-level rules.

  1. Sign into the DigitalOcean console.
  2. Open API → Personal access tokens .
  3. Click Generate New Token. Name it tovinio-billing (or whatever your naming convention uses).
  4. Pick a scope strategy. Easiest is the Read Only preset — it covers every endpoint tovinio calls. For least-privilege, choose Custom Scopes and check just account → Read and billing → Read. Leave everything else unchecked.
  5. Set an expiration. 90 days is recommended; an indefinite token is technically accepted but discouraged.
  6. Copy the token from the next screen — DigitalOcean shows it once. Paste it into the tovinio connection form before navigating away.

DO's PAT UI offers three top-level paths: the Read Only preset, the Full Access preset, and Custom Scopes. tovinio works with any of them but only needs two read actions.

Scopes — what to pick in the console

Type:           Personal access token
Easy mode:      Preset → Read Only   (covers every endpoint tovinio calls)

Least privilege (Custom Scopes):
                account   → Read     (1/1)
                billing   → Read     (1/1)
                everything else      (leave unchecked)

Expiration:     90 days (recommended). Indefinite tokens accepted but discouraged.
  1. account → Read — reads the account profile to confirm the token is alive (/v2/account).
  2. billing → Read — lists invoices and pulls each invoice's per-line-item CSV (/v2/customers/my/invoices and …/csv).
  3. No write actions. Skip every Create / Update / Delete checkbox.

Three endpoints, total. Validation hits one; sync hits the other two — the invoices list once, plus one CSV download per invoice.

DO API surface

# Validation — runs on every save and on scheduled re-check:
GET  /v2/account
     → { account: { email, uuid, status, ... } }
       tovinio asserts account.status == "active".

# Sync — runs on backfill, manual sync, and the daily cron:
GET  /v2/customers/my/invoices
     → { invoices: [ { invoice_uuid, invoice_period, amount, ... } ] }
       tovinio paginates the trailing 90 days for a backfill.

GET  /v2/customers/my/invoices/{invoice_uuid}/csv
     → text/csv  (one HTTP call per invoice; usually 3-4 per backfill)
       Each row → a raw row: { service, resource_id?, usd, date }.

Every row of the CSV becomes a raw row in tovinio. The column mapping: productservice; description may carry a resource_id; USDusd; startdate.

Sample (truncated)

product,group_description,description,hours,start,end,USD,project_name,category
Droplets,project-a,prod-web-1 (s-2vcpu-2gb),720.000000,2026-04-01,2026-04-30,12.00,project-a,iaas
Spaces,,Spaces Object Storage,720.000000,2026-04-01,2026-04-30,5.00,,iaas
Block Storage,project-a,vol-prod-data-100gb,720.000000,2026-04-01,2026-04-30,10.00,project-a,iaas
Bandwidth (Public),,Outbound bandwidth (overage),1.000000,2026-04-01,2026-04-30,0.18,,bandwidth
Load Balancers,project-b,lb-prod (small),720.000000,2026-04-01,2026-04-30,12.00,project-b,iaas

DigitalOcean has no AWS-style cost-allocation tags. tovinio uses the project_name column from each invoice CSV — populated by DO's built-in Projects feature on your account — and surfaces it on the raw row as a synthetic project tag. Two rule kinds work against that:

  1. kind=tag — matches the synthetic tag (e.g. rule project = acme matches every CSV line where project_name is acme).
  2. kind=account — matches the account subject returned by /v2/account (your DO team's email or uuid). Useful when one DO team maps to one tovinio project entirely.

To populate project_name, assign each Droplet, Space, and load balancer to a Project in the DO console (Manage → Projects). Resources without a Project assignment ship to tovinio with an empty project_name and end up unallocated.


5,000
DO requests / hour / token
≈ 5
requests per backfill
60s
tovinio cooldown / connection

DigitalOcean enforces a per-token rate limit of 5,000 requests per hour. A typical 90-day backfill uses one /account call, one invoice list page, and one CSV download per invoice — roughly three to five HTTP calls total. To prevent accidental hammering, tovinio applies a 60-second cooldown between backfills for the same connection.


  1. Console → API → Personal access tokens .
  2. Click the trash icon next to the token. Confirm.
  3. In tovinio, edit the connection and paste a freshly minted token. Validation will run again before the credential is re-encrypted.

If you revoke without replacing, the next sync fails with a sanitized error and the connection moves to the error state. Live data already ingested stays in tovinio; only future syncs are blocked.


  • CSVs reflect billed spend, not real-time usage. Same-day numbers may shift slightly until the invoice is finalized.
  • tovinio's parser tolerates added columns. If DigitalOcean grows the CSV schema, integration keeps working as long as the column names above remain.
  • No resource-level Cost Explorer-style API exists for DO. "Unattributed" rows are the honest signal that no resource id was attached to the line item.

Frequently asked

What credentials does Tovin.io need?

Read-only access to billing data plus a single identity-validation endpoint per provider. No permission to create, modify, or delete cloud resources — ever.

How is each credential stored?

Every credential is encrypted under KMS envelope encryption at rest, decrypted only during a billing pull, and rotated by pasting a fresh credential — never re-keyed in place.

Which providers are supported?

AWS via cross-account IAM role or access key, Google Cloud via a billing-scoped service account JSON key, and DigitalOcean via a read-only personal access token.

Does Tovin.io ever write back to a cloud account?

No. The connection surface is entirely read-only and the docs above list every endpoint each integration calls — there is nothing else.


Colophon

Set in Instrument Serif for display, Geist for body and UI, Geist Mono for code. Printed on warm parchment with oxide-red as the only accent — used here for hairlines, the wordmark dot, and a single hovered link. Any future provider added to tovinio inherits the same chapter shape.

Update cadence

This page is the source of truth for the credential surface. When a new endpoint, scope, or permission is added — or removed — the chapter updates with it. Last revised alongside the per-resource MTD breakdown landing in the drill-down view.