Graine AI
Enterprise

Security

How Graine protects customer data — encryption in transit and at rest, in-app chat and call security, the tenancy boundary, scope-based access control, webhook integrity, retention, sub-processors, and a tamper-evident audit trail you can verify yourself through the API.

This page describes how the Graine platform protects your data: the controls, where each one applies, and what your reviewers can verify for themselves. Where a control is provisioned per deployment or enabled per agent, that is stated with it.

Security is shared: for the split between what Graine does and what you do, for each deployment model, see Security: what Graine does, what you do.

The architecture of protection

Three properties define the shape of the platform's security model. Everything in the sections below is an application of one of them.

Encryption is layered, and the layers are independent. Transport security protects data in motion: the API and the chat and call connections accept TLS 1.2 and 1.3 only, browsers are told to use HTTPS for every request (HSTS), and plain HTTP is redirected with a 301. Live call audio runs over wss://, in-app call media is encrypted between the device and our media servers with DTLS-SRTP, and outbound calls to model, speech and telephony providers are HTTPS. At rest, the primary database, call recordings and backups are kept on storage our providers encrypt with AES-256. On top of that, the platform can encrypt selected fields with AES-256-GCM before they are written — so a database dump, a backup copy or a support read yields ciphertext for those fields — a layer provisioned per deployment, as described below.

Browsers get the protective headers on every response. The dashboard and the API send Strict-Transport-Security, X-Content-Type-Options: nosniff and frame protection, and the dashboard adds a referrer policy and a permissions policy that allows the microphone only where a call needs it. The website widget can only start a conversation on the websites you allow.

Tenancy is resolved from the credential, never from the request. Your organization is derived from the authenticated API key. An organization_id parameter is accepted for API symmetry, but any value other than the key's own organization is a hard 403 — never a silent override.

Key custody is explicit, and it is a deployment property. By default, encryption keys are held by the deployment and can be rotated without re-encrypting stored data. Where envelope encryption is provisioned, each organization gets its own data key wrapped by a managed key-management service (KMS) — optionally under a customer master key held in your own cloud account, which you can revoke. Because custody is set per deployment, scope it explicitly before contract; the answer for the environment you will run against is given in writing.

Data protection

Field-level encryption at rest

PropertyValue
AlgorithmAES-256-GCM — authenticated encryption (AEAD)
IV96-bit, randomly generated per value, never reused
Key custodyThe deployment's key, or a per-organization KMS-wrapped data key
RotationSupported without re-encrypting stored data. Under envelope encryption, each value names the key that encrypted it

Covered fields:

  • Contact phone numbers and originating numbers on call records and on batch contacts.
  • Webhook signing secrets.
  • Every custom webhook header value — not a subset judged to "look sensitive". A heuristic over customer-chosen header names cannot be complete (X-Client-Token, Cookie, X-Signature are all plausible), so the rule is uniform. Your own Authorization: Bearer … for your endpoint is protected against a database dump, a support read or a leaked backup.

This control is provisioned per deployment, and on Graine's shared cloud it is built but not yet switched on. Until its keys are set and the values already stored are encrypted (a backfill we run, which is in progress), phone numbers, webhook signing secrets and webhook header values rely on our database provider's encryption at rest, described above. Where it is switched on, it protects values written from then on, and phone numbers stay searchable through a keyed index. Ask us to confirm the key state of the environment you will be running against; we give it in writing, and we update this page when the shared cloud changes.

A related guarantee runs on read: a secret or header value that cannot be decrypted is dropped rather than sent. The platform will not send an encrypted string as your Authorization header or sign with an unreadable key — the subscription is marked unavailable and the delivery record says so, so the condition is visible rather than silent.

Per-tenant envelope encryption

Where it is provisioned, field encryption runs under a three-level key hierarchy rather than one deployment key:

KMS master key  (Graine's, or your own account's under BYOK)
   │  wraps
   ▼
per-organization data key (AES-256)
   │  encrypts — AES-256-GCM
   ▼
field values — contact and originating numbers, webhook secrets, header values

The master key never sees field data; it only wraps your organization's data key. A destroyed or withdrawn key stops being usable within 15 minutes.

Two properties are worth a reviewer's attention:

A value is bound to its organization. Each encrypted value carries the identity of the key that encrypted it as authenticated data, so a value copied between tenants fails authentication instead of returning another organization's phone number.

Customer-managed keys (BYOK). An organization can be configured against a master key in its own cloud account. Withdrawing Graine's grant on that key stops every future decryption — within the 15 minutes above, not indefinitely. Moving an organization onto its own key does not rewrite any stored value.

Crypto-shredding. Destroying an organization's data key makes its encrypted data unreadable without touching a single record. It is deliberately hard to invoke by accident: it needs a typed confirmation, a closed account or two distinct people, a waiting period of at least 24 hours, and a check that no unencrypted copy of the fields remains. Every step — including an attempt that was refused — is written to the audit trail described below.

The two dates are stated precisely: the data is immediately unreadable in production, and permanently unrecoverable once the backup retention window has elapsed, since a restore from an older backup would bring the key back. You are given that second date.

Envelope encryption is provisioned per deployment and is off by default.

Transcript PII redaction

Transcript redaction can be enabled per agent. When it is, emails, payment card numbers (13–19 digits with optional separators), US Social Security numbers, Indian Aadhaar and PAN identifiers, clustered phone-number digits and IBAN-shaped account numbers are stripped before the transcript is written to storage — this is not a read-time mask over stored plaintext. Patterns are applied longest-match-first so no fragments survive, and both transcript shapes are walked: a plain string, and a structured turn-by-turn list.

Redaction is pattern matching. It reduces exposure of the identifier classes above; it does not recognise names, so treat personal names and free-text identifiers as retained. A change to the setting applies within five minutes.

Access control

One credential on the public API

The /v2 API accepts exactly one credential: an API key with a gat_ prefix, presented as a bearer token.

curl -sS https://api.graine.ai/v2/user/me \
  -H "Authorization: Bearer gat_YOUR_API_KEY"

The credential is inspected before any network call is made, so a browser session token presented to /v2 is rejected outright rather than exchanged for access:

{
  "error": 1101,
  "message": "A browser session token is not a public API key. Authenticate with Authorization: Bearer gat_..."
}

A leaked dashboard session can never drive the partner API. A revoked key stops working within 30 seconds, and a key that is not tied to an organization is rejected rather than allowed to run an unscoped query. Keys are kept in an access-controlled secrets store, encrypted at rest.

Errors follow one envelope across the whole surface:

CodeHTTPMeaning
1100401Missing, unknown, or inactive API key
1101401A browser session token was presented instead of an API key
1102403Valid key, but not for this organization, or lacking a required scope
1200404No such resource in this organization
1300429Request rate exceeded, or concurrency exhausted

Scopes on every mutating endpoint

Eleven scopes:

agents:read    agents:write   calls:read     calls:write
batches:write  webhooks:read  webhooks:write keys:read
keys:write     account:read   audit:read

They are enforced on every mutating /v2 endpoint — every POST, PATCH, PUT and DELETE — and the same check authenticates, meters and authorises every request. Reads are scoped too: every route called with a key checks a scope except GET /v2/user/me and GET /v2/scopes. An automated check fails our build if any mutating route is missing from the published scope table, so a route without a scope does not ship.

A denial returns 1102/403 and writes an audit row (scope.denied), so "our integration stopped working after a key change" is a query, not an investigation. POST /v2/api-keys takes an explicit scopes array and can never grant a scope the minting key does not itself hold. GET /v2/scopes publishes the taxonomy and what the calling key holds. Wildcards resource:* and * are honoured. Full rules are in Authentication.

Two properties worth knowing when you plan a key policy:

Keys can be minted with an explicit scope set. Keys issued before scopes existed carry no scope field and remain unrestricted, so live integrations keep working. An explicitly empty list [] grants nothing. Every audit row records whether the key's authority was explicit or inherited, so "how many keys still hold full access" is a query with an answer. Rotate older keys onto explicit scopes; nothing else narrows them.

keys:write is never granted implicitly. It is deny-by-default and deliberately absent from the compatibility grant, so no key in circulation can mint or revoke another key without that scope being set explicitly.

The dashboard

The dashboard signs people in through a managed business identity provider, and every signed-in request is checked against it. A revoked session or a changed role takes effect within one minute.

Authorization is role-based. Every member of a workspace is an Admin, a Member or View only, and each role holds a fixed set of permissions — agents.delete, campaigns.run, billing.manage, team.manage and so on. Team roles has the whole table. Three properties for a review:

  • Every action declares its permission. An automated check fails our build when a dashboard action does not check its permission, and the API enforces the same permissions for dashboard sessions and for API keys.
  • The role is read live. It is resolved from the member's current record on each request, never from the snapshot taken at sign-in, so a demotion takes effect without waiting for the person to sign out — and a role change never signs anyone out. View only is a mark that only Graine's servers can write.
  • A refusal is explicit. A 403 names the missing permission and the caller's role ("code": "permission_denied"), and the refused attempt is recorded in the audit trail like any other action — see Monitoring and audit.

API keys follow the person who made them: a key a Member creates is limited to member scopes and is refused admin-only actions, so a key never does more than its creator could do in the dashboard.

Rate limiting and concurrency governance

Rate limiting is per organization, not per key, so an organization cannot multiply its quota by minting more keys. Each request is counted atomically, so two concurrent requests cannot both observe headroom.

RequestsLimit
Placing calls — creating calls, campaigns and batches, scheduling a batch, stopping or transferring a call600 / minute (500 sustained)
Reading calls — listing calls and executions, and reading a call's transcript, recording, timing and usage600 / minute (500 sustained)
Knowledge-base uploads20 / minute
Transliteration60 / minute
Everything else1,000 / minute

These are the standard limits; a plan can raise them. Every authenticated /v2 response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a denial is a 429 with error 1300 and a Retry-After. It is on for every /v2 route — there are no deliberately unmetered routes. Rate limits has the full rules.

Separately, live call capacity is bounded at four levels — the platform, your organization, each agent and each campaign. Each slot is taken atomically, so two calls can never both take the last one, and capacity is reconciled continuously so a call that ended is never counted against you for long. See Rate limits.

Tenant isolation

Every /v2 route resolves your organization from your credential, as described at the top of this page. Three properties make the boundary hold:

A cross-tenant identifier answers 404, never 403. An identifier belonging to another organization is indistinguishable from one that does not exist, so ids cannot be probed for existence. Cross-organization reads are refused with "not found" and logged as blocked.

The organization is part of every read, not a filter afterwards. Records are fetched with your organization as a condition of the lookup itself, never fetched first and checked later.

This is not configurable and there is no bypass.

Integrate against /v2

/v2 is the supported public API. Build integrations against it only — other paths serve Graine's own dashboard and are not a public interface.

In-app chat and calls

The chat window and calls your app opens with our SDKs — and the website widget — connect through one authenticated door. The publishable key in your app is public by design, so it is never what authenticates a conversation.

Connection tickets

Before a chat, or a call over a socket, opens, the SDK asks for a connection ticket. It is issued only after the agent's checks pass: the publishable key, your allowed websites and apps, and — when you turn them on — verified apps and verified users. A ticket is:

  • signed with HMAC-SHA256, and the signature is checked before anything in the ticket is read;
  • short-lived — valid for 120 seconds;
  • single use — a second connection with the same ticket is refused, and if the record of used tickets cannot be checked, the connection is refused rather than allowed;
  • bound to one agent — it cannot be spent on another agent, even in the same workspace;
  • sent in the WebSocket handshake, never in the URL, so it does not land in access logs.

Who the conversation is with travels inside the signed ticket, never as something a page or an app can set afterwards, and only our servers write the fields that name a user.

The SDKs fetch a fresh ticket for every connection, so a reconnect is never refused for reusing one. Nothing ties a ticket or a conversation to a network address: a phone that moves between Wi-Fi and mobile data reconnects from its new address and is admitted like any other connection.

In-app calls over WebRTC get their own credential for each call, issued for one agent, and their media is encrypted between the device and our media servers with DTLS-SRTP — WebRTC has no unencrypted mode. A call's screen channel joins only a call that is live.

Limits that never end a conversation

Conversations are limited per device, per user and per workspace — your plan's figure, signed into each ticket so that no page can raise it. When the app or the user is identified, nobody is limited by their network address: many phones share one address on mobile networks. Anonymous website visitors are counted per browser where the widget has an id for it, with a generous per-address limit as a backstop against abuse.

A limit is checked only when a conversation starts. Reaching one refuses the new conversation with a code that names the limit; it never ends a conversation in progress. Message limits work the same way: the burst of messages that arrives when a network recovers gets through, a message longer than 4,000 characters is shortened rather than refused, and only sustained abuse closes a connection.

Masked on the device

The SDKs mask PAN, Aadhaar and payment-card numbers, Indian mobile numbers and email addresses in everything the app sends on its own — screen context, variables and user traits — before it leaves the device. What a person types or says to the agent is sent as written, so the agent can understand it; turn on transcript redaction (above) for what is stored.

Who a conversation is with

Your user's id is turned into a one-way key, scoped to your workspace, before memory or past conversations are looked up; memory never sees the id itself.

With only a publishable key, the id is your app's claim. So that someone who knows a person's id cannot read their memory or past conversations from another phone, a device must be bound to the person first: the first two devices (you can allow up to five) are trusted, a device idle for 30 days gives its place back, and you can reset a person's devices from the dashboard. For proof rather than a limit, turn on verified users. See Identity.

Your apps and verified users

Two settings per agent, each off until you turn it on, each with a report-only stage before it enforces:

  • Your apps declares which apps may use the agent's key: the iOS bundle ID and Team ID, or the Android package and signing-certificate fingerprint. On its own a declaration stops casual reuse of a key, not a determined attacker. Require a verified app adds proof — Apple App Attest or Google Play Integrity, checked on our servers and renewed every 24 hours for each install.
  • Verified users proves the identity your app sends: an identity token your server signs, or the ID token from your own sign-in, checked against your published keys, issuer and audience. A proven identity is renewed every hour for each install and user.

App checks apply to apps; a website is checked against your allowed websites. Verified users apply to websites and apps alike.

What memory never stores

Memory is off until you turn it on for an agent. When it is on, everything saved is redacted first: phone numbers, email addresses, Aadhaar and PAN, bank-account and card numbers and UPI IDs, names used as identifiers, addresses and PIN codes, health information (conditions, prescriptions, diagnoses), dates of birth and other government IDs are replaced with placeholders. If the text cannot be redacted, nothing is saved. Redaction is automated, so treat memory as personal data all the same. Memories expire after the retention you choose for the agent; a person can have theirs forgotten from your app, and you can forget them from the dashboard. See Memory.

Retention and deletion

DataHow long it is kept
A conversation's recent messages, so a chat can continueThe last 50 messages, for 7 days by default and at most 30; sent back only to a device bound to that person
Memory, when turned onUntil the retention you set for the agent, then deleted; at once when forgotten
Call records — transcripts, summaries and recordingsFor the life of your workspace, unless you ask us to delete them
Audit trail400 days by default, configurable from 30 to 3,650

The call and chat runtime writes the lines it logs without the words of the conversation: a typed message, a transcript, the input to the model and the reply being spoken are each replaced by their length.

Sub-processors and where data is processed

WhatWhere
The API, chat and call serversMumbai, India
Primary databaseRun by our database provider; the region is confirmed in writing on request
Call recordingsUnited Kingdom
API keysNorthern Virginia, United States
Language modelsAzure OpenAI by default; another provider only if you choose one for an agent. Where each processes data is on request
Speech recognition and voicesThe provider you choose for each agent (Sarvam AI, Deepgram, ElevenLabs, Cartesia and others). Where each processes data is on request
Memory, when turned onOur memory provider; where it processes data is on request
Knowledge bases, when usedUnited States

The full list — every provider by name, what it processes and where — is on the Trust Center. A location reads "On request" until we have confirmed it with the vendor.

Webhook integrity

Signatures over the exact bytes sent

Each delivery carries X-Graine-Signature: t=<unix>,v1=<hex> alongside X-Graine-Timestamp, where the signature is HMAC-SHA256(secret, "<unix_ts>." + body) in lowercase hex.

The signed bytes are the bytes on the wire. The payload is serialised once and never re-serialised, so your verification can never fail on JSON key ordering or float formatting — the failure mode integrators most often get burned by elsewhere. The signing function refuses anything but raw bytes.

The timestamp is inside the signed material, not merely alongside it, so a captured POST cannot be replayed later under a fresh header: recompute over the t you received and reject a stale one. Graine is the sender here; the staleness threshold is yours to choose and enforce at your endpoint.

Custom headers you configure are applied after the platform's own and filtered: anything named content-type, host or content-length is dropped, so a custom header cannot misstate the body length or the content type. A CR or LF in a header name or value drops that header — a request-splitting and log-forging guard — and the name is escaped before it reaches a log line.

Treat X-Graine-* as reserved. Do not configure a custom header with a platform header's name — such as X-Graine-Signature, which your verifier depends on. Identify a delivery by X-Graine-Delivery-Id and X-Graine-Attempt rather than by the request's User-Agent, which is not part of the signed material and is not a stable contract.

Outbound request vetting that defeats DNS rebinding

Before a callback URL is used, its host is resolved and every resolved address is checked. An address is blocked when it is not globally routable, or is loopback, link-local, private, reserved, multicast or unspecified — including shared address space (100.64.0.0/10) and IPv4 addresses wrapped in IPv6 forms.

Vetting a name is not enough, so the request is pinned to the vetted address. The outbound request URL is rewritten to the vetted IP literal while the original hostname is kept in the Host header and in TLS SNI and certificate verification. Your virtual hosting and your certificate work exactly as before, but a second DNS answer cannot change where the bytes go. Redirects are not followed — a 3xx is a retry, never a silent hop to an unvetted host.

A DNS failure is not treated as a block: it surfaces as a retryable dns_error on your delivery record rather than a permanent rejection. Logs carry the callback host only — never a full URL (query strings routinely carry tokens), never a header value, never a signing secret.

No duplicate sends, no lost deliveries

Every delivery attempt is claimed exactly once: a duplicate or replayed attempt is rejected, and the retry sequence can never move backwards and resend an attempt that already went out.

Retries are scheduled durably — not as in-memory timers — so a deployment in the middle of a retry sequence does not lose a webhook, and your payload is never stored with the scheduled job. A sweep every two minutes re-drives any delivery that has fallen behind. The worst case is a late delivery, never a lost one.

The sequence is 4 attempts at +0s, +10s, +60s and +120s from the first attempt, with a 30-second timeout each. Every attempt is recorded with its result — timeout, TLS error, DNS error, transport error, or the HTTP status.

Monitoring and audit

An append-only, hash-chained audit trail

Every state-changing /v2 action — and every change a person makes in the dashboard — writes one row to a per-organization, append-only, hash-chained log. Rows are never updated; the only deletion is retention.

How the chain works:

  • Each row's hash is SHA-256 over a canonical JSON serialisation (sorted keys, fixed separators) of its fields — which include the sequence number and the previous row's hash. Deleting a middle row and re-linking its successor would require rewriting every later hash, and each rewrite is itself a detectable mismatch.
  • Two rows can never share a sequence number, so a duplicated write cannot fork the chain.

GET /v2/audit-logs/verify walks the chain and reports discrepancies by kind:

FindingWhat it means
hash_mismatchA row was edited — an outcome, a resource id, a timestamp
broken_link + missing_recordA middle row was deleted; two independent signals fire, not one
missing_record against the headRows were removed from the newest end — including a total wipe
head_mismatchThe tip was rewritten
unsealed_record / unreadable_recordA row was written or stored malformed

Retention pruning verifies clean and is distinguishable from tampering: the surviving chain links to a recorded prune point.

You can verify independently. Every listed row publishes its own hash and prev_hash, so an exported log can be re-verified outside Graine entirely.

EndpointPurpose
GET /v2/audit-logsList rows for your organization, filtered and paged
GET /v2/audit-logs/{event_id}One row
GET /v2/audit-logs/verifyWalk the chain and report discrepancies

All three require the audit:read scope. Verification is bounded and resumable — 500 rows per call by default, 2,000 maximum; when next_seq returns non-null, pass it back as start_seq.

Admins read the same trail in the dashboard's Activity log: one timeline of who did what, to what, and whether it was allowed, filtered by person, kind of change and dates, exportable as CSV, with a Verify integrity check that walks the chain and reports the first break. Members and View only seats cannot open it (Team roles).

What a row records

Each row carries who (for a dashboard action, the member and the role they held at the time; for an API key, its key id, developer id and name), what (action, resource type, resource id), the outcome (success / denied / failure), the HTTP status and error code, the request origin, and a changes summary — field names, counts and identifiers. The request body is never stored. A 4xx is recorded as denied and a 5xx as failure, because "who was told no" and "what broke" are different questions. Every scope denial is a row, and reads of the audit log itself are audited.

Coverage is every state-changing /v2 route, every change made in the dashboard, and control events — including an attempt a person's role refused, and changes Graine makes itself, such as pausing a campaign that ran out of credits. A few settings live only in the dashboard — web components, workflows, the website embed, compliance documents, the GSTIN — and their changes go to a second per-organization chain the dashboard keeps, hashed the same way: each row's SHA-256 covers its sequence number and the previous row's hash. The Activity log shows and verifies both. Reads of business data are not audit events, and there is no SIEM export today — export from the Activity log, or read the API on a schedule, if you need the rows in your own tooling.

Retention is 400 days by default, configurable per organization between 30 and 3,650 days.

One property worth stating precisely

An audit row is written a few milliseconds after the response, so recording an action can never slow or fail a customer's call. If a row is ever lost in that moment, the loss is not silent: verification reports a missing_record at exactly that position. "We have a complete audit trail" is a query you can run, not a claim you have to take on faith.

Automated tests

The controls on this page are covered by automated tests — scope enforcement on every route, the audit chain (including a total deletion), webhook signing and retries, tenant isolation, and atomic capacity. We run them before changes ship; our build pipeline runs the main API's and the voice runtime's suites automatically, and we are extending it to every service. They complement, and are not a substitute for, a penetration test.

Certifications and attestations

FrameworkStatus
SOC 2Type I in progress; Type II to follow
ISO/IEC 27001:2022In progress
India's Digital Personal Data Protection Act, 2023Controls aligned; programme in place
HIPAANot claimed. We do not offer a Business Associate Agreement (BAA) today

Graine does not currently hold a SOC 2 (Type I or Type II) report or an ISO/IEC 27001 certificate, and no third-party penetration test report is available today.

The controls described above are implemented in the platform and verifiable now: tenant isolation and scope enforcement can be exercised against your own keys, and the audit chain can be verified through GET /v2/audit-logs/verify or re-verified independently from an exported log. They cover cryptographic controls and key rotation, logical access control and least privilege, tenant separation, integrity of outbound communication, and a tamper-evident record of administrative activity, with implementation detail we will supply in writing on request. None of this is a substitute for, or a claim to, a third-party attestation.

The Trust Center lists every control with its status, and our subprocessors and policies. We will complete a security questionnaire, provide architecture detail, or arrange a technical review with the engineers who wrote these controls.

Your apps, your users and the Security page

In-app agents can be restricted to your own apps, require a proof from App Attest or Play Integrity, and require each user to be proven by your sign-in — see Secure your agent. The workspace Security page shows every agent's protection, sub-accounts, two-step sign-in, API keys and the access log — see Secure your workspace. The push credentials Graine rings your app's users with, and the Play Integrity credential, are kept write-only and encrypted under a key-management service — how each one is protected, scoped, audited, rotated and revoked is in How Graine handles your keys.

Raising a security question

  • Security questionnaires (CAIQ, VSA, or your own template) — send it to your Graine representative. Answers are given in writing, including where the answer is "not implemented"; nothing is deferred to a sales conversation.
  • Architecture review — we will put the engineers who built the audit chain, the tenancy resolver and the delivery engine on a call with your reviewers.
  • Evidence requests — deployment configuration, the current provider list reachable from your agent configuration, the encryption key state for your environment, and storage access policy are all available as evidence items on request.
  • Reporting a vulnerability — contact your Graine representative directly. Please do not open a public issue.