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

POST/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 202 with a checkoutUrl: 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

PATCH/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

GET/subscriptions/meAnyone signed in, or a key with files:read.
Response
{
  "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

GET/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

GET/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
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: date and files. A quiet day is 0, so every day has a point and a chart needs no gap-filling.
byEmployeearrayoptional
Who uploaded: fullName, files, bytes and lastUploadAt, most active first. People who have since been removed are still counted.
storageobjectoptional
liveFiles and liveBytes (what exists right now, ignoring the range), and uploadedBytesInRange (including files deleted since).
quotaobjectoptional
The current billing period, whatever the range: the plan, limit, used, and a points list with, for each day, used so far and pace.
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

GET/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 file or employee.
from / toISO date-timeoptional
A time window: from inclusive, to exclusive.
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"
One entry
{
  "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. null for 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

AreaActions
Company and accountcompany.registered, company.activated, company.activation_resent, company.updated, user.profile_updated
Sign-inauth.password_reset_requested, auth.password_reset, auth.password_changed, auth.identity_linked, auth.identity_unlinked
Peopleemployee.invited, employee.invite_resent, employee.accepted_invite, employee.disabled, employee.reactivated
Plans and billingsubscription.created, subscription.changed, billing.checkout_started, billing.invoice_finalized, billing.payment_failed, billing.payment_succeeded, billing.company_suspended, billing.company_reactivated
API keysapi_key.created, api_key.revoked
Filesfile.uploaded, file.access_changed, file.deleted, report.rebuild_requested
Commentscomment.created, comment.updated, comment.deleted
Rulesquality_rule.created, quality_rule.updated, quality_rule.deleted
Webhookswebhook_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 carry via: "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?

GET/auth/meNeeds a session.

Returns the signed-in person and their company in one call:

Response
{
  "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

PATCH/users/me
curl
curl -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

PATCH/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/forgot with your email sends 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/reset with the link's token and a newPassword sets 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 is 409.

Company details

PATCH/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.