Skip to content

Trust & legal

Security

AVRA decides whether a product is genuine. That answer is only worth something if the machine behind it can be described. This page describes it.

Every claim below names the mechanism that makes it true and where to check it. We do not hold a security certification. When we start one, it will appear in In progress at the bottom of this page with a date, and it will stay there until a certificate exists.

What the platform does, and how you can check it

Tenancy and access

Tenancy and access: claim, mechanism, and how to check it
ClaimMechanismCheck it
One customer can never read another customer's data.Every query is scoped to the caller's tenant before it runs. A request for another tenant's record answers 404, never 403 — a 403 would confirm the record exists.Ask us for a login on a second tenant and try. Trace: docs/architecture.md "Security plan"; backend/tests/test_master_detail.py.
Console access is invite-only. There is no public sign-up.Every login is invited by email and the person invited chooses their own password. A login is disabled from the same screen, and its session dies with it.Visit /login. There is no sign-up link, and the page says why. Trace: backend/tests/test_brand_logins.py.
The company that prints your labels cannot take over your account.Your brand's login is invited by email and you set its password yourself, so the printer never holds it. There is one live login per brand.Trace: migration y9a0b1c2d3e4f (invited logins); backend/tests/test_brand_logins.py.
Passwords are hashed, and resetting one ends every session issued before it.bcrypt at cost 12 in production, enforced by a startup guard that refuses to boot below it. Reset tokens are single-use, hashed at rest, and expire in 60 minutes. Sessions issued before a reset or change are rejected by a tokens_valid_from check.Reset your password on one browser and refresh another. Trace: backend/app/startup.py; backend/app/auth.py.
The forgot-password screen never tells an attacker whether an email exists.One neutral response for every address, rate-limited to 5 requests a minute per IP.Try it with an address that is not ours.

The codes themselves

The codes themselves: claim, mechanism, and how to check it
ClaimMechanismCheck it
A code cannot be guessed.10 characters from a 31-character alphabet with no look-alikes, drawn from the operating system's cryptographic random source — roughly 8 × 10¹⁴ possible codes, against a rate limit of 120 scans a minute.Trace: backend/app/services/code_generator.py; docs/architecture.md "Code format".
Batch Activation — a stolen print file verifies nothing.Codes are created dead. A batch moves generated → exported → activated, and only the console's Activate step, taken after printing QC, makes a code answer anything but We can't verify this code. An overrun, a leaked file or a stolen roll all scan that way.Generate a batch in a sandbox tenant, download the file, and scan a code before activating. Trace: BatchStatus in backend/app/models.py.
Dual Code — a photographed shelf label cannot be replayed.Two factors on one label. The visible QR answers "this label is real" and is meant to be scanned by anyone. The hidden code under the scratch panel claims the unit exactly once. Photographing the shelf QR copies the half that claims nothing.Trace: the scan-rules table in docs/architecture.md; backend/tests/test_product_lines.py.
Two people scanning the same code at the same instant cannot both be told "genuine".The claim is one atomic conditional UPDATE — the count is raised only where it is still below the limit — not a read-then-write. The reward claim on top of it has race tests that only run on PostgreSQL; the scan claim's own race is not separately tested, and we say so.Trace: backend/app/services/scan_service.py; backend/tests/test_claims_pg.py.
The scratch code under the foil is not stored anywhere, in any form.It is derived: HMAC-SHA256(per-batch secret, label number), where the secret is encrypted with the brand's own data key. Verification re-derives and compares in constant time; the export re-derives the same way. There is no column to leak.Trace: backend/app/services/scratch.py; docs/architecture.md "Scratch codes are cash".
Five wrong attempts at a hidden code lock that label for a day — and a wrong code is recorded as a counterfeit signal, not a typo.A fake pack carries a fake code. scratch_wrong is its own scan result and feeds the alert engine; the lock is its own result, scratch_locked.Trace: MAX_ATTEMPTS and LOCK_HOURS in backend/app/services/scratch.py; ScanResult in backend/app/models.py.
Print files stop being downloadable once the batch is live.Partners download freely until activation. Afterwards the file is platform-admin only, every download is audited, and the file is purged 30 days after activation. The in-console QR preview follows the same rule and audits every look.Trace: docs/architecture.md "The export contract"; the batch.codes_view audit action.

Money and the ledger

Money and the ledger: claim, mechanism, and how to check it
ClaimMechanismCheck it
The credit ledger cannot be edited. Only appended.credit_ledger is insert-only. A refund is a second row against the same batch, never an edit of the first, so a balance cannot drift from its history.Open the Wallet page and reconcile the balance against the rows yourself.
A retried upload can never generate the same batches twice.Batch generation from an upload is idempotent on a request_key; the same key returns the existing batch instead of generating a second one.Trace: backend/app/services/batch_csv.py.
Two batches generated at the same instant cannot overspend a balance.One atomic conditional UPDATE deducts the credits; the generation that loses the race is refused.Trace: docs/architecture.md "Business model: resellers and credits".
Reward money moves on the brand's own payment account, never through AVRA's.Each brand connects its own Razorpay X or Cashfree account. AVRA holds no float and is not in the money's path. If AVRA disappears tomorrow, the account is still the brand's.Trace: docs/briefs/D-payout-providers.md; the per-brand webhook route /api/public/payouts/{provider}/webhook/{brand_id}.
The wallet is an append-only ledger with one writer.Every movement — earn, redeem, payout, reversal, adjust, expire — goes through a single ledger.post() that locks the membership row before it writes.Trace: backend/app/services/ledger.py.
Payout credentials cannot be read back out of the system, by anyone, including us.Credentials are envelope-encrypted per brand under a master key that is not in the database backups. They reach the payment adapter as an object whose repr prints no value, and the Settings screen says they cannot be read back.Save a key, then try to read it. Trace: docs/deployment.md "The master key"; backend/app/services/payout_providers/base.py.

The scan path

The scan path: claim, mechanism, and how to check it
ClaimMechanismCheck it
The consumer verdict page makes no third-party request.The typeface is self-hosted, the logo is inline SVG, the city lookup is an offline database file on our own disk, and the India map is a local GeoJSON with no tile provider. Nothing on /s/, /b/, /c/ or /me/ reaches another company's server.Open the verdict page with your network tab recording and count the origins. There is one. Trace: docs/architecture.md "The two maps".
A buyer's location is approximated from their IP against a file we hold — their IP is never sent to a geolocation service.An offline DB-IP City Lite database, refreshed monthly. City level only, never coordinates. If the lookup fails it never fails the scan.Trace: docs/deployment.md; backend/app/services/geo.py.
The scan endpoint is rate-limited without breaking shared mobile networks.120 requests a minute per IP — high on purpose, because Indian mobile carriers put thousands of real buyers behind one address, and a naive limit would block a market.Trace: scan_rate_limit_per_min in backend/app/config.py.
Our error tracking cannot leak your codes.The error tracker is scrubbed at the source: stack-frame locals are never attached, URLs and query strings are redacted, request bodies never leave the server, and the scrubber fails closed — a partially scrubbed event is dropped. The reason is specific: a dual-mode label's hidden code can appear in a URL.Trace: backend/app/monitoring.py; docs/deployment.md "Error tracking".

Data handling

Data handling: claim, mechanism, and how to check it
ClaimMechanismCheck it
Every sensitive action is written to an insert-only audit log.Logins, credit issues, batch generations, activations, blocks and cancellations, print-file downloads, code previews, payout-account changes, claim voids, exports, and member searches.Ask us for an audit extract for your own tenant.
No raw SQL runs anywhere in the platform.Queries go through the ORM only; every string input is length-limited by a schema at the edge.Trace: the engineering standard in CLAUDE.md.
A spreadsheet we produce cannot execute a formula on your machine.Every CSV cell we emit passes one guard that neutralises a leading =, +, -, @, tab, CR or LF — print files included. Excel exports write every cell as a string.Trace: backend/app/services/csv_safe.py; backend/tests/test_export_contract.py generates a batch named =cmd|' /c calc'!A1.
An uploaded image cannot carry anything but pixels.Images are re-encoded server-side to WebP with camera metadata stripped, and capped at 12 megapixels.Upload a photo with GPS EXIF and read the stored file. Trace: backend/app/services/uploads.py.
Timestamps are stored in UTC, everywhere, and displayed in IST.One utcnow() helper server-side; one lib/ist.ts on the display side.Trace: CLAUDE.md; frontend/src/lib/ist.ts.
The database is not reachable from the internet.PostgreSQL runs in a private subnet, reached over a peered network with SSL required. It has no public endpoint.Trace: docs/deployment.md.
Backups run nightly and can be restored to a point in time.Managed automated backups with a 7-day point-in-time window, plus our own nightly pg_dump to object storage with the restore recipe in the script's own header.Trace: scripts/backup.sh; docs/deployment.md.

What GENUINE actually means

When the verdict page says GENUINE, it is asserting four things and nothing more:

  1. This code was generated by AVRA for this brand, and is not a string that happens to look like one.
  2. Its batch was activated by a person after printing QC. Before that moment this code answered We can't verify this code.
  3. This verification is within the batch's verification limit — the label has not already been claimed by someone else.
  4. Nobody has blocked the batch since.

It is not asserting that the physical object in your hand is the one the label was stuck to, or that the pack has not been opened. No QR code can assert either. Those are jobs for something physical — a hologram or OVD foil, a destructible face stock, a VOID-release adhesive, a tamper band, a seal that tears — and it is why AVRA is sold alongside the pack rather than instead of it. Which formats can carry which is stated per substrate for printers and packaging companies.

Suspicious scans, named as rules

Suspicious scans: what we watch for, what it usually means, what happens
What we watch forWhat it usually meansWhat happens
One code verified from many distinct networks, beyond its limit, inside a weekA code was photographed off a shelf label and reprinted onto a run of fakesA clone-attack alert on your dashboard naming the code and the cities
Ten or more of your not-yet-activated labels scanned in a weekYour print file leaked, or a roll left the print floor earlyAn unactivated-codes alert — the earliest warning in the system, because those codes have not shipped
Five or more scans a week of codes AVRA never generatedSomebody is printing invented codes onto packaging that imitates yoursA fake-sighting alert to the platform admin, who routes it to the brand
A wrong hidden code typed under a real shelf QRA fake pack with a guessed code under the panelRecorded as a counterfeit signal, not a typo. Five wrong tries lock that label for 24 hours
A reward claim that breaks a rolling 24-hour capOne person working a pile of packs, or a recycled-code schemeThe claim is held, not rejected — money stops before it leaves, and a person releases or rejects it
A reward campaign that has spent its budgetNot fraud — but the same stopNo claim card is shown at all, and the brand is emailed at 80% and 100%

What we do not claim to catch: a counterfeiter who buys genuine product, scrapes the codes from the labels and never sells the originals. No code-based system catches that, ours included. Batch Activation and per-unit claiming raise the cost of a fake from “photograph one label” to “buy every unit you intend to copy”, which is the honest description of what this defends.

What happens with no signal

A phone with no signal cannot verify anything, and we will not pretend otherwise. The verdict comes from our server, because the answer depends on facts that cannot live on the label: whether the batch was activated, whether this code has been claimed already, and whether the same code is being scanned in four cities at once. A label that could answer offline would be a label a counterfeiter could answer with too.

What we do instead: the verdict page is built to load on a slow connection on a cheap Android phone, with no third-party requests, and a buyer in a basement can walk outside and scan again. Re-scanning your own product never consumes a verification.

Your data: ownership, residency, retention

The scan data is the brand's

Every scan of your labels, every warranty registration, every reward claim and every fake report belongs to you. AVRA processes it on your instruction. We do not sell it, do not license it, do not aggregate it into a market report, and do not show it to another brand — including a brand that shares your partner.

What you can take as a file today, without asking us: warranty registrations, payout statements, the TDS file and your print files, from the console. Scan history, the credit ledger and member lists are on screen but not yet a file — the export list says which is which, and until those exist we produce them on request.

If you leave, ask us to delete your tenant and we delete it, except the reward, redemption and payout records that Indian tax law requires be kept for eight years. Those are financial records, not marketing data, and we would be breaking the law to delete them.

And the labels already on shelves keep working. A label you have paid for keeps answering, for the life of the batch, whether or not you buy another credit. A person holding a real product must not be told it is fake because two companies stopped doing business. The rest is on what happens if you leave.

We process your data in India

The database, the application servers and the print files run in AWS Mumbai (ap-south-1). The database has no public endpoint. Nothing is replicated outside India by us.

One caveat we state rather than bury: if you download a statement of your own payouts and your parent company is outside India, you have made that transfer, not us — and you are responsible for it. The statements carry masked phone numbers and no PANs.

Sub-processors, what each handles, and where
Sub-processorWhat it handlesWhere
Amazon Web ServicesApplication servers, database, backups, print-file storageIndia (ap-south-1)
Meta / WhatsApp Business PlatformThe one-time code that signs a member in, and the transactional message templatesMeta's own infrastructure
Your own payment gateway (Razorpay X or Cashfree)Reward payouts, on your account and under your contract. AVRA never holds the funds.India
Our email providerPassword-reset and alert emailNot yet confirmed in writing — see In progress
Sentry, when error tracking is switched onError reports, scrubbed before they leave our serverNot yet confirmed in writing — see In progress

The city lookup is not a sub-processor. We do not call a geolocation service. We hold a database file and read it. Your buyers' IP addresses never leave our servers.

How long we keep it

Retention: what is kept, for how long, and what happens then
WhatKeptThen
A scan: the full IP address30 daysTruncated to the network prefix. The counterfeit rule counts networks over a week, and a prefix serves that as well as a host address.
A scan: the browser user-agent90 daysReduced to a family, such as Chrome/Android.
A scan: the browser identifier400 daysCleared. The cookie itself lasts a year; the column outlives it by a month.
A scan: the row itselfFor everIt is the product. After the sweeps above it holds no personal data.
A fake report: the reporter's phone number90 daysErased.
A print file, after the batch is activated30 daysPurged.
Rewards, redemptions, payouts8 yearsFinancial record — Indian tax law.
A member's profile (name, UPI id, PAN)Until they close the accountErased on close; the phone number stays on the financial record.

These sweeps run daily on a background worker and each one is idempotent. They are a job, not a promise on a page. Trace: backend/app/services/retention.py; backend/tests/test_retention.py.

What your partner can and cannot see

The company that prints your labels can see:

  • The brands, products and batches it created for you, and the labels it printed.
  • How many credits it bought and spent on your account, and when each batch was generated and by whom.
  • Your scan counts, your genuine rate, your scans by city, and your alerts.
  • Fake reports filed against your products — it works them on your behalf, and every status change is audited.
  • Print files for your batches, until the batch is activated. After that, only a platform admin can download them, and every download is audited.
  • Your rewards member list, claims and ledger — only if you admit it. This is a switch you own and it is off by default.

It cannot see:

  • Any other partner's anything. The request answers 404.
  • Your payout credentials, or your members' PANs. Both are encrypted and are not readable by staff of any kind.
  • Your hidden or scratch codes after activation. The console's QR preview follows the same download rule and audits every look.
  • Your rewards programme at all, unless you admitted it.

Your buyers are told this too. Version 3 of our consumer notice states, in the consent a member ticks, that the partner who prints your labels may administer the programme for you. We do not disclose to you something we hide from them.

How we count visits to this website

Our own copy of Umami, an open-source analytics tool, runs on the same server as this site: no cookie, no stored IP address, and nothing sent to anyone else. A session identifier is a one-way hash that changes at the start of every calendar month. The verification pages — /s/…, /verify and the rewards pages — load no analytics of any kind. The full wording is in the privacy notice.

Reporting a vulnerability

Email connect@avrascan.com. We do not run a PGP key, and we would rather say so than publish one nobody reads.

What you get from us: an acknowledgement from a person, an assessment, and word when it is fixed. If you want credit, we will name you here. If you want to stay anonymous, that is fine too.

What we ask: give us a reasonable window before publishing, and test against your own tenant and your own codes. Do not run scanners against the consumer verdict path — a live scan burns a real buyer's verification, and we would rather give you a sandbox tenant. Ask and you will have one the same day.

We do not pay bounties. We would rather say so than let you spend a weekend expecting one.

In scope: avrascan.com and every subdomain, the console, the consumer verdict pages, and the public brand report links. Out of scope: our brands' own websites, our partners' own systems, the payment gateways (report those to the gateway), and anything requiring physical access to a print floor. Machine-readable: /.well-known/security.txt.

In progress, not yet true

No target date is set for any of these yet. When one is, it appears here, and the item stays until it is done.

In progress: what is not yet true, why it matters, and the target date
Not yet trueWhy it mattersTarget
Two-factor authentication on console loginsToday a console account is a password. For a platform that can block a live batch, that is one factor too few.Not set
Rate limiting that survives more than one serverThe limiter holds its counters in one process's memory. It works, and it will stop working the day we run two application servers.Not set
Uptime monitoring, backup-failure alerting, and a restore drill we have actually runBackups run. We have not yet proven a restore under time pressure, and an untested backup is a hope.Not set
An independent penetration testEverything on this page is our own account of our own system.Not set
A signed data-processing agreement template, and a partner agreementToday these are negotiated per deal. They should be downloadable.Not set
A published deadline for deleting a departed brand's dataWe delete on request. We have not yet committed to a number of days, and a number nobody enforces is worse than none.Not set
Naming the hosting region of our error tracker and email providerBoth are listed below without a region, because we have not confirmed it in writing.Not set
Two-way payout reconciliation on CashfreeCashfree publishes no list-transfers API, so our nightly both-ways payout check cannot run for a brand on that gateway. We say so on the Settings screen when the gateway is chosen.Not fixable by us; it needs the gateway

Not on this list, deliberately: ISO 27001 and SOC 2. It will appear when an auditor is engaged, with the auditor's name and the expected certificate date.

Print one batch and scan it yourself.

The fastest way to judge this is a sheet of real labels and a phone.

Trust, made visible.

connect@avrascan.com · +91 91737 47583 · Navacara Infotech, Ahmedabad, Gujarat, India