{"components":{"responses":{},"schemas":{"CreateBucketRequest":{"properties":{"bucket_name":{"example":"my-new-bucket","type":"string"},"versioning":{"default":false,"type":"boolean"}},"required":["bucket_name"],"title":"CreateBucketRequest","type":"object"},"TrialLimits":{"description":"Trial configuration and limits, discoverable without creating a trial. Sourced from the same config signup enforces, so it can never diverge from what POST /trials actually grants.","properties":{"docs":{"example":"https://docs.ramo.io/trial","format":"uri","type":"string"},"max_buckets":{"example":3,"type":"integer"},"quota_bytes":{"example":1073741824,"format":"int64","type":"integer"},"s3_endpoint":{"example":"https://s3.ramo.io","format":"uri","type":"string"},"signup_limit":{"properties":{"max_per_ip":{"example":5,"type":"integer"},"window_hours":{"example":24,"type":"integer"}},"required":["max_per_ip","window_hours"],"type":"object"},"ttl_days":{"example":365,"type":"integer"}},"required":["quota_bytes","max_buckets","ttl_days","signup_limit","s3_endpoint","docs"],"title":"TrialLimits","type":"object"},"Credit":{"description":"A credit applied to, or applicable to, a customer's invoices.","properties":{"amount_cents":{"example":2500,"minimum":1,"type":"integer"},"granted_by":{"example":"ops@example.com","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"invoice_id":{"description":"ID of the invoice this credit is attached to, if any.","format":"uuid","nullable":true,"type":"string"},"reason":{"example":"Goodwill credit","type":"string"},"type":{"enum":["manual","reimbursement"],"type":"string"}},"required":["id","type","amount_cents","reason","inserted_at"],"title":"Credit","type":"object"},"PortalCredential":{"description":"Credential metadata (no secret).","properties":{"access_key":{"example":"AKIAIOSFODNN7EXAMPLE","type":"string"},"bucket_names":{"items":{"type":"string"},"type":"array"},"customer_id":{"nullable":true,"type":"string"},"description":{"nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"policy":{"enum":["readwrite","readonly","writeonly"],"type":"string"},"principal_name":{"example":"cust-rw-abc12","type":"string"},"status":{"enum":["desired","provisioning","active","rotating","revoked"],"type":"string"}},"required":["id","access_key","principal_name","policy","bucket_names","status","inserted_at"],"title":"PortalCredential","type":"object"},"OperationCount":{"properties":{"count":{"type":"integer"},"date":{"format":"date","type":"string"},"operation":{"example":"PUT","type":"string"}},"required":["date","operation","count"],"title":"OperationCount","type":"object"},"CreateTokenRequest":{"properties":{"description":{"nullable":true,"type":"string"},"expires_at":{"description":"Ignored (and forced to the trial's expiry) for trial customers.","format":"date-time","nullable":true,"type":"string"},"scopes":{"description":"Subset of buckets/credentials/billing. Omit for the default set.","items":{"enum":["buckets","credentials","billing"],"type":"string"},"type":"array"}},"title":"CreateTokenRequest","type":"object"},"CreditsResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Credit"},"type":"array"}},"required":["data"],"title":"CreditsResponse","type":"object"},"InvoicesResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Invoice"},"type":"array"}},"required":["data"],"title":"InvoicesResponse","type":"object"},"TrialGrant":{"description":"Everything needed to use standard S3 tooling against a new trial bucket. The secret key and portal token are shown once here and never retrievable again.","properties":{"access_key":{"example":"AKIA...","type":"string"},"bucket":{"example":"trial-my-demo-x7k2","type":"string"},"docs":{"example":"https://docs.ramo.io/trial","format":"uri","type":"string"},"endpoint":{"example":"https://s3.ramo.io","format":"uri","type":"string"},"expires_at":{"format":"date-time","type":"string"},"limits":{"$ref":"#/components/schemas/TrialLimits"},"portal_token":{"description":"Scoped portal token; drives the existing portal API.","example":"fmk_...","type":"string"},"quota_bytes":{"example":1073741824,"format":"int64","type":"integer"},"secret_key":{"description":"Shown once; never stored in plaintext.","type":"string"}},"required":["endpoint","bucket","access_key","secret_key","portal_token","quota_bytes","expires_at","docs","limits"],"title":"TrialGrant","type":"object"},"Error":{"description":"Error envelope matching the existing Phoenix JSON error shape.","properties":{"errors":{"additionalProperties":true,"description":"Either `%{detail: \"...\"}` for generic errors, or `%{field: [\"...\"]}` for validation errors.","type":"object"}},"required":["errors"],"title":"Error","type":"object"},"NodeResult":{"description":"Provisioning result for a single MinIO node.","properties":{"node_id":{"example":"minio-1","type":"string"},"reason":{"description":"Present on failure.","nullable":true,"type":"string"},"status":{"example":"ok","type":"string"}},"required":["node_id","status"],"title":"NodeResult","type":"object"},"ClaimResponse":{"description":"The customer now bound to the caller's Clerk identity.","properties":{"claimed_at":{"format":"date-time","type":"string"},"customer_id":{"example":"cust-abc","type":"string"},"kind":{"enum":["standard","trial"],"type":"string"}},"required":["customer_id","kind","claimed_at"],"title":"ClaimResponse","type":"object"},"StorageBucket":{"description":"A provisioned S3 bucket.","properties":{"acl":{"enum":["private","public_read","public_read_write"],"type":"string"},"bucket_name":{"example":"my-bucket","type":"string"},"customer_id":{"example":"cust-abc","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"status":{"enum":["desired","provisioning","active","deleting","deleted"],"type":"string"},"versioning":{"example":false,"type":"boolean"}},"required":["id","bucket_name","versioning","acl","status","inserted_at"],"title":"StorageBucket","type":"object"},"CreateTrialRequest":{"properties":{"name":{"description":"Optional label slugified into the bucket name.","example":"my-demo","nullable":true,"type":"string"}},"title":"CreateTrialRequest","type":"object"},"PortalTokenSecretResponse":{"description":"Returned once at mint/rotate time. Contains the plaintext token.","properties":{"data":{"$ref":"#/components/schemas/PortalToken"},"token":{"description":"Shown exactly once.","example":"fmk_...","type":"string"},"warning":{"type":"string"}},"required":["data","token","warning"],"title":"PortalTokenSecretResponse","type":"object"},"InvoiceResponse":{"properties":{"data":{"$ref":"#/components/schemas/Invoice"}},"required":["data"],"title":"InvoiceResponse","type":"object"},"PrefixSnapshotResponse":{"description":"Per-bucket prefix snapshot for the authenticated customer.","properties":{"data":{"items":{"properties":{"bucket_name":{"example":"nn-staging-w3m","type":"string"},"prefixes":{"items":{"properties":{"object_count":{"example":12345,"format":"int64","minimum":0,"type":"integer"},"prefix":{"description":"Path prefix relative to the bucket; null for bucket-root objects.","nullable":true,"type":"string"},"total_bytes":{"example":9876543210,"format":"int64","minimum":0,"type":"integer"}},"required":["prefix","object_count","total_bytes"],"type":"object"},"type":"array"},"snapshot_at":{"description":"Time the snapshot was materialized; null if the bucket is enrolled but not yet snapshotted.","format":"date-time","nullable":true,"type":"string"}},"required":["bucket_name","snapshot_at","prefixes"],"type":"object"},"type":"array"}},"required":["data"],"title":"PrefixSnapshotResponse","type":"object"},"RetrievalBytes":{"properties":{"bytes":{"type":"integer"},"date":{"format":"date","type":"string"}},"required":["date","bytes"],"title":"RetrievalBytes","type":"object"},"ClaimRequest":{"description":"Proves ownership of a customer account to bind it to the caller's Clerk identity.","properties":{"portal_token":{"description":"Plaintext portal token for the customer being claimed.","example":"fmk_...","type":"string"}},"required":["portal_token"],"title":"ClaimRequest","type":"object"},"PortalCredentialCreateResponse":{"description":"Returned once at creation time. Contains the plaintext secret_key.","properties":{"data":{"properties":{"access_key":{"type":"string"},"principal_name":{"type":"string"},"secret_key":{"description":"Shown exactly once.","type":"string"},"warning":{"type":"string"}},"required":["access_key","secret_key","principal_name","warning"],"type":"object"},"node_results":{"items":{"$ref":"#/components/schemas/NodeResult"},"type":"array"}},"required":["data","node_results"],"title":"PortalCredentialCreateResponse","type":"object"},"PortalCredentialsResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/PortalCredential"},"type":"array"}},"required":["data"],"title":"PortalCredentialsResponse","type":"object"},"BucketsResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/StorageBucket"},"type":"array"}},"required":["data"],"title":"BucketsResponse","type":"object"},"CreateCredentialRequest":{"properties":{"bucket_names":{"description":"Must all be owned by the caller.","items":{"type":"string"},"type":"array"},"description":{"nullable":true,"type":"string"},"policy":{"enum":["readwrite","readonly","writeonly"],"type":"string"}},"required":["policy"],"title":"CreateCredentialRequest","type":"object"},"NodeResultsResponse":{"properties":{"node_results":{"items":{"$ref":"#/components/schemas/NodeResult"},"type":"array"}},"required":["node_results"],"title":"NodeResultsResponse","type":"object"},"Invoice":{"description":"An invoice for a billing period.","properties":{"credits":{"description":"Credits attached to this invoice. Present only on the detail endpoint.","items":{"$ref":"#/components/schemas/Credit"},"type":"array"},"credits_applied_cents":{"example":2500,"type":"integer"},"customer_id":{"type":"string"},"generated_at":{"format":"date-time","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"line_items":{"description":"Present only on the detail endpoint.","items":{"$ref":"#/components/schemas/InvoiceLineItem"},"type":"array"},"period_end":{"format":"date","nullable":true,"type":"string"},"period_start":{"format":"date","nullable":true,"type":"string"},"source":{"enum":["metered","manual"],"type":"string"},"status":{"enum":["draft","finalized","void"],"type":"string"},"subtotal_cents":{"example":12500,"type":"integer"},"total_cents":{"example":10000,"type":"integer"}},"required":["id","customer_id","subtotal_cents","credits_applied_cents","total_cents","status","source","inserted_at"],"title":"Invoice","type":"object"},"PortalToken":{"description":"Token metadata (no hash, no plaintext).","properties":{"description":{"nullable":true,"type":"string"},"expires_at":{"format":"date-time","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"last_used_at":{"format":"date-time","nullable":true,"type":"string"},"revoked_at":{"format":"date-time","nullable":true,"type":"string"},"scopes":{"items":{"type":"string"},"type":"array"}},"required":["id","scopes","inserted_at"],"title":"PortalToken","type":"object"},"PortalCredentialRotateResponse":{"description":"New secret returned exactly once.","properties":{"data":{"properties":{"secret_key":{"description":"New secret. Shown exactly once.","type":"string"},"warning":{"type":"string"}},"required":["secret_key","warning"],"type":"object"},"node_results":{"items":{"$ref":"#/components/schemas/NodeResult"},"type":"array"}},"required":["data","node_results"],"title":"PortalCredentialRotateResponse","type":"object"},"InvoiceLineItem":{"description":"A single line on an invoice.","properties":{"amount_cents":{"example":6250,"type":"integer"},"category":{"enum":["storage","request","retrieval"],"type":"string"},"description":{"example":"Cold storage, January 2026","type":"string"},"id":{"format":"uuid","type":"string"},"operation":{"example":"PUT","nullable":true,"type":"string"},"product_type":{"enum":["cold_storage","hot_storage","storage_service"],"nullable":true,"type":"string"},"quantity":{"description":"Decimal string (e.g. TiB-months or request count)","example":"12.5","type":"string"},"unit_price_cents":{"example":500,"type":"integer"}},"required":["id","category","description","quantity","unit_price_cents","amount_cents"],"title":"InvoiceLineItem","type":"object"},"UsageResponse":{"properties":{"data":{"$ref":"#/components/schemas/UsageSnapshot"}},"required":["data"],"title":"UsageResponse","type":"object"},"BucketStorage":{"description":"Live storage usage for a single bucket.","properties":{"bucket_name":{"example":"grass-bucket","type":"string"},"current_storage_bytes":{"description":"Bytes currently stored in this bucket. 64-bit unsigned value reflecting the running counter maintained by the storage metering pipeline.","format":"int64","minimum":0,"type":"integer"}},"required":["bucket_name","current_storage_bytes"],"title":"BucketStorage","type":"object"},"BucketStorageResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/BucketStorage"},"type":"array"}},"required":["data"],"title":"BucketStorageResponse","type":"object"},"UsageSnapshot":{"description":"Point-in-time usage view for a customer across a date range.","properties":{"current_storage_bytes":{"description":"Live snapshot of bytes stored right now; not range-bounded.","type":"integer"},"customer_id":{"type":"string"},"from":{"format":"date","type":"string"},"operation_counts":{"items":{"$ref":"#/components/schemas/OperationCount"},"type":"array"},"retrieval_bytes":{"items":{"$ref":"#/components/schemas/RetrievalBytes"},"type":"array"},"to":{"format":"date","type":"string"}},"required":["customer_id","from","to","current_storage_bytes","operation_counts","retrieval_bytes"],"title":"UsageSnapshot","type":"object"},"PortalTokenResponse":{"properties":{"data":{"$ref":"#/components/schemas/PortalToken"}},"required":["data"],"title":"PortalTokenResponse","type":"object"},"BucketResponse":{"properties":{"data":{"$ref":"#/components/schemas/StorageBucket"},"node_results":{"items":{"$ref":"#/components/schemas/NodeResult"},"type":"array"}},"required":["data","node_results"],"title":"BucketResponse","type":"object"},"PortalTokensResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/PortalToken"},"type":"array"}},"required":["data"],"title":"PortalTokensResponse","type":"object"}},"securitySchemes":{"BearerAuth":{"description":"Customer API token (prefix `fmk_`). Required on all portal endpoints.","scheme":"bearer","type":"http"}}},"info":{"description":"Public customer-facing API. Serves invoices, credits, usage, buckets,\nand credentials for the authenticated customer, plus anonymous trial\nsignup.\n\n**Authentication:** all endpoints except `POST /api/portal/trials`\nrequire a Bearer token in the `Authorization` header\n(`Authorization: Bearer <token>`). Trial signup returns a scoped token\nusable on the rest of this API. Missing or invalid tokens return\n`401 Unauthorized`; a token without the scope an endpoint requires\nreturns `403 Forbidden`.\n","title":"Ramo Storage Portal API","version":"0.1.0"},"openapi":"3.0.0","paths":{"/api/portal/buckets":{"get":{"callbacks":{},"description":"Returns all provisioned buckets owned by the current customer.","operationId":"DataOnboardingIngressWeb.Portal.BucketController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BucketsResponse"}}},"description":"List of buckets"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token missing required scope"}},"security":[{"BearerAuth":[]}],"summary":"List buckets for the authenticated customer","tags":["Buckets"]},"post":{"callbacks":{},"description":"Creates the bucket on all active nodes and records desired state. Returns per-node provisioning results. The bucket is scoped to the caller's customer.","operationId":"DataOnboardingIngressWeb.Portal.BucketController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBucketRequest"}}},"description":"Bucket attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BucketResponse"}}},"description":"Bucket created"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token missing required scope, trial quota exceeded, or trial bucket limit reached"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Bucket name already taken or not empty"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid bucket name"}},"security":[{"BearerAuth":[]}],"summary":"Provision a new bucket","tags":["Buckets"]}},"/api/portal/buckets/{name}":{"delete":{"callbacks":{},"description":"Deletes the bucket from all nodes. Refused if the bucket is non-empty or if the bucket does not belong to the authenticated customer (404).","operationId":"DataOnboardingIngressWeb.Portal.BucketController.delete","parameters":[{"description":"Bucket name","in":"path","name":"name","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeResultsResponse"}}},"description":"Bucket deleted"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token missing required scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Bucket not found or not owned by this customer"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Bucket not empty or node unreachable"}},"security":[{"BearerAuth":[]}],"summary":"Delete a bucket","tags":["Buckets"]}},"/api/portal/claim":{"post":{"callbacks":{},"description":"Presents a portal token proving ownership of a customer account and binds the caller's Clerk identity to it. One-to-one in both directions: an identity can claim only one customer, and a customer can be claimed by only one identity.","operationId":"DataOnboardingIngressWeb.Portal.ClaimController.claim","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimRequest"}}},"description":"Claim request","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimResponse"}}},"description":"Claim succeeded"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a Clerk dashboard session"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Identity already bound, or customer already claimed"},"410":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The customer's trial has expired"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid portal token or insufficient token scopes"}},"security":[{"BearerAuth":[]}],"summary":"Bind the authenticated Clerk identity to a customer","tags":["Claim"]}},"/api/portal/credentials":{"get":{"callbacks":{},"description":"Returns credential metadata only. The secret key is never included.","operationId":"DataOnboardingIngressWeb.Portal.CredentialController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortalCredentialsResponse"}}},"description":"List of credentials"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token missing required scope"}},"security":[{"BearerAuth":[]}],"summary":"List credentials for the authenticated customer","tags":["Credentials"]},"post":{"callbacks":{},"description":"Creates a MinIO service account and returns the access key and secret key exactly once. The `bucket_names` list must contain only buckets owned by the caller.","operationId":"DataOnboardingIngressWeb.Portal.CredentialController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCredentialRequest"}}},"description":"Credential attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortalCredentialCreateResponse"}}},"description":"Credential created (secret shown once)"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token missing required scope"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid params or bucket not owned by caller"}},"security":[{"BearerAuth":[]}],"summary":"Provision a new credential","tags":["Credentials"]}},"/api/portal/credentials/{access_key}":{"delete":{"callbacks":{},"description":"Revokes the service account on all nodes and removes it from Redis. Returns 404 if the credential is not owned by the caller.","operationId":"DataOnboardingIngressWeb.Portal.CredentialController.delete","parameters":[{"description":"Access key ID","in":"path","name":"access_key","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeResultsResponse"}}},"description":"Credential revoked"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token missing required scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Credential not found or not owned by this customer"}},"security":[{"BearerAuth":[]}],"summary":"Revoke a credential","tags":["Credentials"]}},"/api/portal/credentials/{access_key}/rotate":{"post":{"callbacks":{},"description":"Generates a new secret key for the credential. The new secret is returned exactly once.","operationId":"DataOnboardingIngressWeb.Portal.CredentialController.rotate","parameters":[{"description":"Access key ID","in":"path","name":"access_key","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortalCredentialRotateResponse"}}},"description":"New secret returned once"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token missing required scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Credential not found or not owned by this customer"}},"security":[{"BearerAuth":[]}],"summary":"Rotate a credential secret","tags":["Credentials"]}},"/api/portal/credits":{"get":{"callbacks":{},"description":"Returns all credit ledger entries for the current customer, newest first.","operationId":"DataOnboardingIngressWeb.Portal.CreditController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditsResponse"}}},"description":"List of credits"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token missing required scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown customer"}},"summary":"List credits for the authenticated customer","tags":["Credits"]}},"/api/portal/invoices":{"get":{"callbacks":{},"description":"Returns invoices for the current customer, newest period first.","operationId":"DataOnboardingIngressWeb.Portal.InvoiceController.index","parameters":[{"description":"Filter by invoice status.","in":"query","name":"status","required":false,"schema":{"enum":["draft","finalized","void"],"type":"string"}},{"description":"Maximum rows to return (default: 50, max: 200).","in":"query","name":"limit","required":false,"schema":{"maximum":200,"minimum":1,"type":"integer"}},{"description":"Pagination offset (default: 0).","in":"query","name":"offset","required":false,"schema":{"minimum":0,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoicesResponse"}}},"description":"List of invoices"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token missing required scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown customer"}},"summary":"List invoices for the authenticated customer","tags":["Invoices"]}},"/api/portal/invoices/{id}":{"get":{"callbacks":{},"operationId":"DataOnboardingIngressWeb.Portal.InvoiceController.show","parameters":[{"description":"Invoice ID (UUID).","in":"path","name":"id","required":true,"schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceResponse"}}},"description":"Invoice detail"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token missing required scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invoice not found or not owned by this customer"}},"summary":"Fetch a single invoice with its line items and credits","tags":["Invoices"]}},"/api/portal/tokens":{"get":{"callbacks":{},"description":"Returns token metadata only. The token hash and plaintext are never included.","operationId":"DataOnboardingIngressWeb.Portal.TokenController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortalTokensResponse"}}},"description":"List of tokens"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a Clerk dashboard session"}},"security":[{"BearerAuth":[]}],"summary":"List portal tokens for the authenticated customer","tags":["Tokens"]},"post":{"callbacks":{},"description":"Mints a token scoped to any subset of buckets/credentials/billing. Trial customers have `expires_at` forced to the trial's expiry and are refused unless the trial is active. Returns the plaintext exactly once.","operationId":"DataOnboardingIngressWeb.Portal.TokenController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTokenRequest"}}},"description":"Token attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortalTokenSecretResponse"}}},"description":"Token created (plaintext shown once)"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a Clerk dashboard session, or the active-token cap is reached"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The customer's trial is not active"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid scopes or expires_at"}},"security":[{"BearerAuth":[]}],"summary":"Mint a new portal token","tags":["Tokens"]}},"/api/portal/tokens/{id}":{"delete":{"callbacks":{},"description":"Revokes the token. Returns 404 if it is not owned by the caller.","operationId":"DataOnboardingIngressWeb.Portal.TokenController.delete","parameters":[{"description":"Token ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortalTokenResponse"}}},"description":"Token revoked"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a Clerk dashboard session"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token not found or not owned by this customer"}},"security":[{"BearerAuth":[]}],"summary":"Revoke a portal token","tags":["Tokens"]}},"/api/portal/tokens/{id}/rotate":{"post":{"callbacks":{},"description":"Mints a clone with the same scopes and description, and revokes the original, atomically. For a trial customer, the clone's expires_at is re-pinned to the trial's current expiry rather than copied from the old token. Returns the plaintext exactly once.","operationId":"DataOnboardingIngressWeb.Portal.TokenController.rotate","parameters":[{"description":"Token ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortalTokenSecretResponse"}}},"description":"New plaintext shown once"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not a Clerk dashboard session, or the active-token cap is 0"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token not found or not owned by this customer"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The customer's trial is not active"}},"security":[{"BearerAuth":[]}],"summary":"Rotate a portal token","tags":["Tokens"]}},"/api/portal/trials":{"post":{"callbacks":{},"description":"Provisions a trial customer, a scoped portal token, a quota-capped bucket, and a readwrite credential in one call. No authentication required; the per-IP daily limit is the anti-abuse backstop.","operationId":"DataOnboardingIngressWeb.Portal.TrialController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTrialRequest"}}},"description":"Trial signup","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrialGrant"}}},"description":"Trial created"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid request body"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Per-IP daily trial limit reached"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Trial signup disabled or failed"}},"security":[],"summary":"Create an anonymous trial signup","tags":["Trials"]}},"/api/portal/trials/limits":{"get":{"callbacks":{},"description":"Quota, bucket cap, TTL, and per-IP signup limit an agent can check before (or instead of) calling POST /trials. Sourced from the same config signup enforces, and gated on the same enabled flag.","operationId":"DataOnboardingIngressWeb.Portal.TrialController.limits","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrialLimits"}}},"description":"Trial limits"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Trial signup disabled"}},"security":[],"summary":"Trial configuration and limits","tags":["Trials"]}},"/api/portal/usage":{"get":{"callbacks":{},"description":"Returns a point-in-time usage view:\n- `current_storage_bytes`: live bytes stored right now (not range-bounded)\n- `operation_counts`: per-day/operation request counts within the range\n- `retrieval_bytes`: per-day retrieval byte totals within the range\n","operationId":"DataOnboardingIngressWeb.Portal.UsageController.index","parameters":[{"description":"ISO-8601 date, inclusive. Defaults to the first day of the current month.","in":"query","name":"from","required":false,"schema":{"format":"date","type":"string"}},{"description":"ISO-8601 date, inclusive. Defaults to today.","in":"query","name":"to","required":false,"schema":{"format":"date","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}},"description":"Usage snapshot"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token missing required scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown customer"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid date params (malformed or reversed range)"}},"summary":"Usage snapshot for the authenticated customer","tags":["Usage"]}},"/api/portal/usage/buckets":{"get":{"callbacks":{},"description":"Returns the live storage usage per bucket for every bucket registered to\nany of the customer's active contracts. Sorted alphabetically by\n`bucket_name`. Buckets with no recorded storage yet are returned with\n`current_storage_bytes: 0`, so output is dense.\n\nReports current state only; does not accept a date range. Historical\nbreakdowns will be added under a separate endpoint when needed.\n","operationId":"DataOnboardingIngressWeb.Portal.UsageController.buckets","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BucketStorageResponse"}}},"description":"Per-bucket storage breakdown"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token missing required scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown customer"}},"summary":"Per-bucket current storage for the authenticated customer","tags":["Usage"]}},"/api/portal/usage/prefixes":{"get":{"callbacks":{},"description":"Returns the latest per-prefix storage breakdown for every enrolled bucket.\nRefresh cadence and depth are configured per customer; the response includes\nthe `snapshot_at` timestamp so callers can show data freshness.\n\nReturns 404 if the customer is not enrolled in prefix snapshotting (no\nconfig row, or config row with `enabled = false`).\n\nBuckets enrolled but not yet snapshotted return `snapshot_at: null` and\n`prefixes: []`. Bucket-root objects are reported with `prefix: null`.\n","operationId":"DataOnboardingIngressWeb.Portal.UsageController.prefixes","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrefixSnapshotResponse"}}},"description":"Per-bucket prefix snapshot"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Token missing required scope"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Prefix snapshots not enabled for this customer"}},"summary":"Per-bucket prefix snapshot for the authenticated customer","tags":["Usage"]}}},"security":[],"servers":[{"url":"https://api.ramo.io","variables":{}}],"tags":[]}