A promotion meant for one customer gets redeemed by one person many times: a first-order discount on ten new accounts, a referral reward for referring yourself, a free trial started again every month. Promo abuse survives limits per account or per email address, because accounts and addresses are free. A limit per device does not.
This guide enforces one redemption per device for promo codes, referral rewards and free trials with Fingerly, from the client to a database constraint.
Where to identify
| Moment | Tag | Why |
|---|---|---|
| Applying a coupon or promotion code | promo:<code> | The tag binds the identification to one promotion. |
| Claiming a referral reward | referral:<referrer id> | Compare the new customer's device with the referrer's. |
| Starting a free trial | trial-start | One trial per device. |
Enforce one redemption per device
Step 1: Identify when the visitor redeems
import { load } from '@fingerly/web-js' // consent: 'granted' only once you have consent (or another lawful basis) const fingerly = await load({ apiKey: 'fly_pk_development_…', consent: 'granted' }) async function submit() { let requestId: string | undefined try { ({ requestId } = await fingerly.identify({ tag: 'promo:' + code })) } catch { // Carry on: your server treats a missing request ID as missing evidence. } await fetch('/api/promotions/redeem', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, requestId }) }) }Step 2: Let the database enforce the limit
Store the visitor ID with every redemption, and put a unique index on the promotion and the visitor ID. Two redemptions racing each other cannot both succeed, because the database refuses the second.
CREATE TABLE redemptions ( code text NOT NULL, visitor_id text NOT NULL, account_id bigint NOT NULL, PRIMARY KEY (code, visitor_id) );Step 3: Read the event and apply the promotion
The server code below uses two helpers from the use-case recipes:
verifiedEvent(requestId, tag), which reads the stored event with a secret key and returns it only when it belongs to the action, is recent and has not been used before, andfired(event, ...groups), which says whether a signal group fired.import { fired, verifiedEvent } from './fingerly.server' export async function redeem(accountId: string, code: string, requestId: unknown) { const event = await verifiedEvent(requestId, 'promo:' + code) // A promotion is optional: without evidence, ask for more before applying it. if (!event || fired(event, 'fingerprint_suppressed')) return 'verify' if (event.suspect_level === 'high') return 'refuse' const inserted = await redemptions.insertIfAbsent({ code, visitorId: event.visitor_id, accountId }) return inserted ? 'apply' : 'already-redeemed' }redemptions.insertIfAbsentis your own query: an insert that reports whether the row was new. Without a usable identification, the device cannot be counted, so ask for verification before applying the offer.
Stop referral fraud
A referral reward is abused when the referrer and the new customer are the same person. Identify when the reward is claimed, with the referrer in the tag, and compare the claiming device with the devices the referrer has used.
export async function isSelfReferral(referrerId: string, requestId: unknown) {
const event = await verifiedEvent(requestId, 'referral:' + referrerId)
if (!event) return true // no evidence, no reward
return accounts.hasUsedDevice(referrerId, event.visitor_id)
}
Create the account either way, and pay no reward when it returns true. For the same reason, store the visitor ID with every account at sign-up, as in detect multiple accounts.
Stop free trial abuse
Use the same tag and table with a trial instead of a code: identify with trial-start, store the visitor ID with the trial, and allow one trial per device. A device that starts a second trial is told it has been used, or is offered the paid plan.
A starting policy
| Situation | Action |
|---|---|
| The device already redeemed this promotion | Refuse the discount, and say it has already been used on this device. |
| A referral where the new customer's device has been used by the referrer | Create the account, but pay no reward. |
high level | Refuse the promotion. |
fingerprint_suppressed fired, or no usable identification | Ask for verification, such as a phone number, before applying it. |
| Otherwise | Apply it. |
Signals that matter for promotions
fingerprint_suppressed: the device hides enough to get a new visitor ID every time, which would defeat a per-device limit.device_farm,android_emulator,ios_simulatorandcloned_app: many "devices" that are really one person's setup.bot: redemptions scripted at scale.residential_proxyandvpn: used to make repeated sign-ups look like different households.
Common questions
How do I prevent promo code abuse?
Identify when the code is applied, read the stored event on your server, and store the visitor ID with the redemption under a unique index on the code and the visitor ID. A second redemption from the same device then fails, whatever account or email it uses.
Can people get around a per-device limit?
A determined user can use another device, so the limit raises the cost of abuse rather than ending it. Combine it with the level and the signals above, and with your own checks such as a verified phone number.
What about customers on a shared device?
They share a visitor ID. If the promotion should allow a household two redemptions, count redemptions per visitor and set the limit above one, or ask for verification when a second arrives.
Does it work for in-app promotions?
Yes. Use the iOS, Android, React Native or Flutter SDK with the same tag, and enforce the same index on your server. A reinstalled Android app gets a new visitor ID, so pair the limit with verification for high-value offers.
How do I handle a visitor that cannot be identified?
Treat it as missing evidence. The answer has identifiable: false and the fingerprint_suppressed signal fires, so ask for verification before applying the offer rather than refusing.