Build with the API
Company and billing API
Plans, invoices, usage analytics, the audit log, and your account and company.
The calls behind Plans and billing, Usage analytics, The audit log and Your account and company.
Plans and billing
Read the plans
The same table is available as data at GET /subscriptions/plans, which needs no sign-in, so a pricing page can read it directly.
Choose a plan
/subscriptions/meAdmins. Your first plan.curl https://gridline-data-analysis-app.duckdns.org/api/subscriptions/me \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "plan": "basic" }'- Free turns on immediately (
201). - Basic and Premium answer
202with acheckoutUrl: a page hosted by our payment provider, Stripe. You enter your card there, never on Gridline. The plan becomes active when Stripe confirms the payment, and not before.
Until a company has a plan, every feature answers 402 Payment Required with "No plan selected yet".
Change plan
/subscriptions/meAdmins. Supports Idempotency-Key.Same body as above. Moving to a paid plan sends you to Checkout; a change between paid plans starts a new billing cycle, and the cost of the part of the old period you used is settled at once; moving to Free cancels immediately.
See where you are
/subscriptions/meAnyone signed in, or a key with files:read.{
"plan": "basic",
"billingAnchorDay": 14,
"period": { "start": "2026-10-14T00:00:00.000Z", "end": "2026-11-14T00:00:00.000Z", "key": "2026-10-14", "days": 31 },
"limits": { "maxEmployees": 10, "maxSeats": 11, "filesPerPeriod": 100 },
"usage": { "files": 37, "employees": 4, "seats": 5 },
"nextDueDate": "2026-11-14T00:00:00.000Z"
}Seats are the admin plus every employee who holds one. A billing period runs from one day-of-month to the next, in UTC.
The running bill
/billing/currentAdmins. Needs billing:read for a key.Shows what the invoice will be if nothing changes: itemised in cents, with the seats, the plan fee and any overage, and when the period ends. It is an estimate until the period closes.
Invoices
GET /billing/invoices: past invoices, newest first.GET /billing/invoices/{id}: one invoice with its line items, a link to Stripe's hosted invoice page, and its PDF.
An invoice's status is one of draft, open, paid, uncollectible or void. All money in the API is in integer cents, never decimals.
To update your card or download receipts, POST /billing/portal-session returns a link to Stripe's customer portal.
Usage analytics
Get the numbers
/analytics/usageAdmins only. API keys cannot use this endpoint; GraphQL offers the same numbers to dashboards.fromdateoptional- First day, inclusive. Default: the first day of the current billing period.
todateoptional- Last day, exclusive. Default: through today. At most 366 days after
from.
Days are UTC days. With neither parameter you get the current billing period so far.
curl "https://gridline-data-analysis-app.duckdns.org/api/analytics/usage?from=2026-10-01&to=2026-11-01" \
-H "Authorization: Bearer ACCESS_TOKEN"What comes back
rangeobjectoptional- The days the numbers cover (
from,to,days). filesPerDayarrayoptional- One point per day:
dateandfiles. A quiet day is0, so every day has a point and a chart needs no gap-filling. byEmployeearrayoptional- Who uploaded:
fullName,files,bytesandlastUploadAt, most active first. People who have since been removed are still counted. storageobjectoptionalliveFilesandliveBytes(what exists right now, ignoring the range), anduploadedBytesInRange(including files deleted since).quotaobjectoptional- The current billing period, whatever the range: the plan,
limit,used, and apointslist with, for each day,usedso far andpace. planHistoryarrayoptional- Plan changes, newest first, up to 50: what changed, when, and what the outgoing plan's part-period cost in cents.
Are you on pace?
pace is where an even spend would be today: included files × days elapsed ÷ days in the period. If used runs above pace, you are on course to hit your quota early. It is the same number the quota alerts at 80% and 100% are about.
The audit log
Read the log
/auditAdmins only. An API key needs the audit:read scope.actionstringoptional- Only one kind of event, such as
file.deleted. actorUserIduuidoptional- Only what one person did.
targetTypestringoptional- The kind of thing acted on, such as
fileoremployee. from / toISO date-timeoptional- A time window:
frominclusive,toexclusive. limit, cursoroptional- Newest first. See Pagination.
curl "https://gridline-data-analysis-app.duckdns.org/api/audit?action=file.deleted&limit=10" \
-H "Authorization: Bearer ACCESS_TOKEN"{
"id": "…",
"action": "file.deleted",
"actorUserId": "…",
"targetType": "file",
"targetId": "…",
"ip": "203.0.113.7",
"correlationId": "7d2f…",
"createdAt": "2026-10-02T09:30:11.482Z"
}The list leaves out each entry's metadata. Read a single entry with GET /audit/{id} to see the details specific to its action, such as the plan changed to or a file's name.
actorUserIduuid | nulloptional- Who did it.
nullfor the system (the nightly billing run) or for a person who has since been removed. correlationIdstringoptional- Ties together every entry one request wrote, and matches that request's logs. Quote it to support.
ipstring | nulloptional- The caller's address, when the change came over the web.
What is recorded
| Area | Actions |
|---|---|
| Company and account | company.registered, company.activated, company.activation_resent, company.updated, user.profile_updated |
| Sign-in | auth.password_reset_requested, auth.password_reset, auth.password_changed, auth.identity_linked, auth.identity_unlinked |
| People | employee.invited, employee.invite_resent, employee.accepted_invite, employee.disabled, employee.reactivated |
| Plans and billing | subscription.created, subscription.changed, billing.checkout_started, billing.invoice_finalized, billing.payment_failed, billing.payment_succeeded, billing.company_suspended, billing.company_reactivated |
| API keys | api_key.created, api_key.revoked |
| Files | file.uploaded, file.access_changed, file.deleted, report.rebuild_requested |
| Comments | comment.created, comment.updated, comment.deleted |
| Rules | quality_rule.created, quality_rule.updated, quality_rule.deleted |
| Webhooks | webhook_endpoint.created, webhook_endpoint.updated, webhook_endpoint.deleted, webhook_endpoint.secret_rotated, webhook_delivery.redelivered |
The list is closed: an action that is not on it cannot be written, and an automated test checks that every action on it really is. A new feature that changes state has to add its own line here.
You can trust it
- Each entry is written in the same step as the change it describes. If the change is rolled back, so is the entry: nothing is logged that did not happen, and nothing happens without a log line.
- The table is append-only at the database level: the database itself refuses to update or delete a row.
- Entries made by a program say so. An API key's entries carry its id in
metadata.apiKeyId, and an AI agent's carryvia: "mcp", so you can always tell a person from a script.
New entries also arrive as they happen on a live connection as audit.appended.
Account and company
Who am I?
/auth/meNeeds a session.Returns the signed-in person and their company in one call:
{
"user": { "id": "…", "email": "nino@acme.com", "fullName": "Nino Beridze", "role": "admin", "status": "active", "activatedAt": "…" },
"company": { "id": "…", "name": "Acme Logistics", "billingEmail": "billing@acme.com", "country": "GE", "industry": "logistics", "status": "active", "isDemo": false }
}Your role and status are read from the database on every request, not copied from your token. So when an admin changes your role or removes you, it takes effect on your very next click.
Your name
/users/mecurl -X PATCH https://gridline-data-analysis-app.duckdns.org/api/users/me \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "fullName": "Nino B." }'Your password
/auth/passwordNeeds a session.currentPasswordstringrequired- To prove it is you.
newPasswordstringrequired- 8 to 128 characters, and different from the current one.
Changing your password signs your sessions out, so anyone who had got hold of an old one is out too.
Forgot it?
POST /auth/password/forgotwith youremailsends a reset link. The answer is always the same, whether or not the address has an account, so nobody can use it to find out who is registered.POST /auth/password/resetwith the link'stokenand anewPasswordsets the new one. A link works once, and only the newest link works.
Sign in with Google
Anyone can use Google instead of a password: to sign in, to register a company, or to accept an invitation. Gridline identifies you by your Google account, never by the email address it reports. That matters:
- You can accept an invitation with a personal Google account whose address differs from the one the invitation was sent to. The invitation link is itself the proof it was meant for you.
- Gridline will link an unknown Google account to an existing person only when Google vouches for the address and it matches exactly one active person. Anything doubtful never links.
- A Google sign-in is tied to your browser with a short-lived cookie, so another person's Google response can never sign in as you.
Linked accounts
GET /auth/identities: how you can sign in today (password, Google).POST /auth/identities/google/link: start connecting a Google account to yourself.DELETE /auth/identities/{id}: disconnect one. You cannot remove your last way to sign in; the answer is409.
Company details
/companies/meAdmins only.namestringoptional- The company's name.
countrystringoptional- Two-letter ISO code, such as
GE. industrystringoptional- One of the industries offered at sign-up.
billingEmailstringoptional- Where invoices and account notices go, including quota emails.
The company is always the one you belong to. You never send a company id, and sending one is refused.
Activation
A new company is inactive until its admin follows the emailed link (GET /auth/activate?token=…). If the email did not arrive, POST /auth/resend-activation with the email sends another. It answers the same whether or not the address is registered.