Account Protection Guide

Protect every account action. Down to the last point.

One endpoint, three decisions, and verifications that Koin runs end to end. With a native data model for loyalty programs.

Loyalty edition · Version 2.3 · October 2026 · English

The whole integration, in two calls

POST /v1/ato/evaluations — Can this action continue?

PATCH /v1/ato/notifications/{evaluation_id} — This is how it ended.

approved · received · denied

References

📘

New

Marks capabilities this guide documents before they are published in the API Reference. Field names may see minor adjustments; the model does not change.

1. The essentials

Whenever a user attempts something sensitive on their account, your backend asks Koin before executing it. Koin responds with one of three decisions:

DecisionWhat it meansWhat you do
approvedNo risk signalsYou continue without friction
receivedA verification is requiredYou show the strategy Koin indicates (token, liveness, 3DS…)
deniedHigh risk or blocked accountYou do not execute the action

When the action ends, you tell Koin how it ended. That is all.

POST  /v1/ato/evaluations          → Can this action continue?
PATCH /v1/ato/notifications/{evaluation_id}   → This is how it ended.

Koin decides on its own. Its models, heuristics, and Koin's data network detect account takeover from day one, without you writing a single rule. If you want, you can customize the decision for your program (rules, lists, thresholds, and strategies per event) from the dashboard. Your integration does not change in either case.

2. Why Koin

KoinWhat is typical in the market
Decides on its own, and can be customizedKoin delivers a ready decision from the first event. Each customer can adjust it to their program without touching the integration.A score the customer has to turn into their own rules before they can use it.
End-to-end verificationKoin sends the token, hosts the page, validates, and reports the final decision. You can also use your own verifications.The provider returns a score or a recommendation; you build the verification.
Native loyaltyPoints balance, tier, beneficiaries, and mixed payments with points are part of the data model.No provider publishes loyalty events: points are modeled as a generic field.
Built for LatAmNative CPF, CNPJ, DNI, CUIL, CI, and PASSPORT; WhatsApp as a channel; its own device SDK (JS, iOS, Android, Flutter).Models designed for the US and Europe.
One identity, two APIsThe same user_id links account events with points transactions: yesterday's suspicious login weighs on today's redemption.Account events and payment events live in separate products.
Open documentationPublic, readable by agents (llms.txt, every page as .md).Many providers keep it behind a login.

3. Quickstart: your first evaluation in 3 steps

Step 1 · Authentication

Every call uses your private key as a Bearer token. The Koin team delivers it during onboarding.

Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

Step 2 · Capture the device

Integrate the device fingerprint SDK (JavaScript, iOS, Android, Flutter). The SDK generates a session_id that you send on every event.

📘

Why it matters

The device is the number-one signal against account takeover. A successful login from a device that has never been seen, followed by an email change, is the classic pattern of a taken-over account.

Step 3 · Evaluate a login

curl -X POST https://api-sandbox.koin.com.br/v1/ato/evaluations \
  -H "Authorization: Bearer $KOIN_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "LOGIN",
    "commerce": "company",
    "details": {
      "user_id": "company_8812345",
      "email": "[email protected]",
      "authentication_type": "PASSWORD",
      "client_result": "SUCCESS"
    },
    "device": {
      "ipv4": "187.61.115.149",
      "session_id": "455e1d2f-6db1-409a-8089-80e40cf37a12",
      "merchant_device": "77370a68-b70e-4a71-a83e-222994416c12"
    }
  }'

Response:

{
  "id": "VEFRUD8HUR25N4992221",
  "evaluation_id": "31ff2cc8-199e-4c3c-ba2d-4d85b88bd9f2",
  "status": "approved",
  "score": 12,
  "analysis_type": "AUTOMATIC"
}

Store the evaluation_id: you will use it to report the final result.

4. Key concepts

The whole model rests on five ideas.

ConceptIn one lineField
EventThe sensitive action you send to Koin so it can evaluate it.type
Verification strategyThe extra step Koin may require before letting the action proceed: token by email or WhatsApp, liveness, 3DS. Koin decides when to apply it; the customer can customize that criterion.strategies[]
Authentication contextThe context of a password change: with an active session, without a session, or initiated by the system. It distinguishes, for example, someone who forgot their password from someone who changes it while logged in.authentication
Prior check by the customerThe result of a validation your system already performed before calling Koin, for example whether the password was correct. It is an input.client_result
Final resultHow the business action ended: it completed or it did not. It is reported later, separately.PATCH New
📘

The prior check and the final result are not the same thing.

client_result happens before the evaluation ("the password was correct"). The final result happens when the whole flow ends ("the email change was completed").

5. How an evaluation works

Every event follows the same path. Verification is conditional: it happens only if Koin responds received, and either Koin or your own system can orchestrate it.

sequenceDiagram
    actor User
    participant Backend as Customer backend
    participant Koin
    User->>Backend: Starts a sensitive action
    Backend->>Koin: POST /v1/ato/evaluations
    Koin-->>Backend: approved, received + strategy, or denied
    opt received and Koin orchestrates the strategy
        User->>Koin: Completes verification on Koin's page
        Koin-->>Backend: Final decision approved or denied via callback_url
    end
    opt received and the customer orchestrates the strategy
        User->>Backend: Completes the customer's verification
        Backend->>Koin: PATCH with the strategy result
        Koin-->>Backend: Final decision approved or denied via callback_url
    end
    Backend->>User: Executes or cancels the action
    Backend->>Koin: PATCH with the final result of the action

Figure 1 · Evaluation lifecycle, with the strategy orchestrated by Koin or by the customer.

📘

The callback carries the decision, not the strategy result.

When there is a verification, Koin combines its result with the rest of the signals and sends you, through callback_url, the final decision of the evaluation: approved or denied.

6. Decisions and strategies

The three decisions

statusMeaningWhat your system does
approvedThe action may continue.Completes the action.
receivedIn progress: a verification is required.Shows the strategy (link) or runs it with its own means, and waits for the final decision through callback_url.
deniedBlock.Does not execute the action. If the account is blocked (from the dashboard or by the engine), all of its events return denied.
📘

On account events (LOGIN, SIGN_UP, CHANGE_PASSWORD, CHANGE_PROFILE, CLOSE_ACCOUNT), received always comes with a strategy the user resolves in the moment. It never leaves the user waiting for a manual review.

Available strategies

strategies[].typeWhat it validatesWhen it is typically used
VerificationCode6-digit token by email or WhatsApp, or magic link.Forgotten password, contact change, account closure.
LivenessFacial biometrics with a proof of life.High risk: contact change after a suspicious login.
CollectAuthRecovery3DS2 authentication (FRICTIONLESS or CHALLENGE).Actions that involve a card.
ManualVerificationReview by the risk team, held until the SLA.High-value operations, never on login.

Example response with a strategy:

{
  "id": "12345678",
  "evaluation_id": "1b943df8-e4fa-4a50-a254-67186911d66e",
  "status": "received",
  "score": 50,
  "analysis_type": "AUTOMATIC",
  "strategies": [
    {
      "type": "VerificationCode",
      "link": "https://antifraud.koin.com.br/recovery/1b943df8-e4fa-4a50-a254-67186911d66e?language=en"
    }
  ]
}

Detail for each strategy: Strategies.

7. Event catalog

A single endpoint, POST /v1/ato/evaluations, and the type field indicates the event. Koin decides in each case whether a verification applies; the "Reference verification" column shows the usual behavior, which each customer can customize.

EventWhen you send itWhen you do notReference verification
LOGINThe user signs in. You report the result of your own credential check (client_result).Do not send only successful logins: failed ones feed credential-stuffing detection.According to the risk Koin detects.
SIGN_UPAccount creation. Requires user_id, first_name, last_name, birth_date, email, and profile (category and points_balance) New.—According to the risk Koin detects.
CHANGE_PASSWORDAny password change or creation. The authentication field indicates the context.—Depends on the context: typically yes when the user has no session, no when they change it while logged in.
CHANGE_PROFILEA piece of data is added, updated, deleted, or confirmed: email, phone, account, card, personal data, category New, or a beneficiary New.Passwords: use CHANGE_PASSWORD. Points balance: it is not reported here (see §10).Token when phone and/or email changes; for the rest, according to the risk Koin detects.
CLOSE_ACCOUNTThe user asks to close their account.—Token, always.

The API Reference also includes USER_ACTIVATION, GENERAL_CHECKPOINT, and ENROLLMENT, for use cases outside the scope of this guide.

Fields common to every event

FieldRequiredWhy it matters
typeYesDefines the event and the details schema.
details.user_idYesThe identity that links every account event, in Account Protection and in the transactional API. Always use the same identifier.
commerceRecommendedIdentifies the merchant or program the event comes from. It lets you separate decisions and reports by brand or channel.
device.session_idRecommendedGenerated by the device SDK. Without it, Koin loses the strongest signal against account takeover.
device.ipv4 / device.ipv6Recommended (one of the two)Geolocation, ASN, and detection of distributed attacks.
device.merchant_deviceOptionalYour own device identifier, so you can cross-check it with Koin's.
callback_urlOptionalHTTPS endpoint where you receive the final evaluation decision when there was a strategy.
sla_dateOptionalHow long the evaluation may stay open.

8. Recipes by event

8.1 LOGIN

{
  "type": "LOGIN",
  "commerce": "company",
  "details": {
    "user_id": "company_8812345",
    "email": "[email protected]",
    "authentication_type": "PASSWORD",
    "client_result": "FAILED",
    "captcha": true,
    "document": { "type": "CPF", "number": "12345678909", "nationality": "BR" }
  },
  "device": {
    "ipv4": "187.61.115.149",
    "session_id": "455e1d2f-6db1-409a-8089-80e40cf37a12"
  }
}

authentication_type: PASSWORD · MAGIC_LINK · SOCIAL_NETWORK

8.2 CHANGE_PASSWORD: one event, three contexts

Every password change uses the same event. The authentication field tells Koin the context it happens in, and that is how the three use cases are distinguished.

flowchart LR
    A["I forgot my password<br/>(no session)"]
    E["CHANGE_PASSWORD"]
    B["Change from my profile<br/>(with session)"]
    C["Forced reset<br/>(support or suspicious activity)"]
    R{"Koin decision"}
    A -->|"authentication: unauthenticated"| E
    B -->|"authentication: authenticated"| E
    C -->|"authentication: system"| E
    E --> R
    classDef dark fill:#0E2419,color:#ffffff,stroke:#0E2419
    class E dark
    classDef decision fill:#E6F6EA,color:#0A7F25,stroke:#10B132
    class R decision

Figure 2 · Password change: 3 contexts, 1 event.

authenticationUse caseReference verification
unauthenticatedI forgot my password. The user, without a session, starts the process. The previous password stays valid until it finishes.Yes (for example, a token).
authenticatedChange from the profile. The user, with an active session, changes their password.Typically no.
systemForced reset. The system resets the password (support or suspicious activity) and the user changes it afterwards.According to the risk Koin detects.

The field accepts only these three values (case insensitive). If you omit it, the event is still processed, but Koin loses the context.

{
  "type": "CHANGE_PASSWORD",
  "commerce": "company",
  "details": {
    "user_id": "company_8812345",
    "email": "[email protected]",
    "authentication": "unauthenticated",
    "client_result": "SUCCESS"
  },
  "device": { "ipv4": "187.61.115.149", "session_id": "455e1d2f-6db1-409a-8089-80e40cf37a12" }
}

8.3 CHANGE_PROFILE: operation × data

CHANGE_PROFILE combines an operation (type) with the data it operates on (field), and carries the previous value and the new one when that applies. Passwords do not go here: they have their own event, CHANGE_PASSWORD.

field ↓ / type →ADDUPDATEDELETECONFIRM
EMAIL✓✓✓✓
PHONE✓✓✓✓
ACCOUNT✓✓
CARD✓✓
PERSONAL_INFO✓
CATEGORY New✓
BENEFICIARY New✓✓✓

Phone or email change: Koin asks for a token to confirm the change.

{
  "type": "CHANGE_PROFILE",
  "commerce": "company",
  "details": {
    "user_id": "company_8812345",
    "type": "UPDATE",
    "field": "PHONE",
    "old_phone": { "area_code": "11", "number": "987654321", "type": "MOBILE" },
    "new_phone": { "area_code": "21", "number": "912345678", "type": "MOBILE" },
    "client_result": "SUCCESS"
  },
  "device": { "ipv4": "187.61.115.149", "session_id": "455e1d2f-6db1-409a-8089-80e40cf37a12" }
}
📘

Special case: email without a password.

If the user changes their email and did not have a password (for example, they signed in with a social network), they must create one. In that case you also send a CHANGE_PASSWORD.

flowchart LR
    A["User changes<br/>email or phone"]
    B["CHANGE_PROFILE"]
    T["Confirmation<br/>token"]
    Q{"Did the email change and<br/>was there no password?"}
    P["CHANGE_PASSWORD"]
    F["End"]
    A --> B
    B --> T
    T --> Q
    Q -->|"Yes"| P
    Q -->|"No"| F
    classDef dark fill:#0E2419,color:#ffffff,stroke:#0E2419
    class B,P dark
    classDef decision fill:#E6F6EA,color:#0A7F25,stroke:#10B132
    class T decision

Figure 3 · Contact change: 2 events, 1 conditional.

Category and beneficiary changes are in the Loyalty Pack.

8.4 CLOSE_ACCOUNT

A single event, always confirmed with a token, whether or not the user has a password.

{
  "type": "CLOSE_ACCOUNT",
  "commerce": "company",
  "details": { "user_id": "company_8812345" },
  "device": { "ipv4": "187.61.115.149", "session_id": "455e1d2f-6db1-409a-8089-80e40cf37a12" }
}
  1. The user asks to close their account.
  2. You send CLOSE_ACCOUNT.
  3. Koin responds received with the strategy (token).
  4. The user validates the token, Koin sends you the final decision, and you execute the closure.
  5. You report the final result.

9. Reporting the final result

New · When the action ends, you report how it ended with a separate notification.

sequenceDiagram
    participant Backend as Customer backend
    participant Koin
    Backend->>Koin: POST /v1/ato/evaluations
    Koin-->>Backend: evaluation_id + decision
    Note over Backend: The user completes or abandons the action
    Backend->>Koin: PATCH /v1/ato/notifications/{evaluation_id}
    Koin-->>Backend: 200 OK

Figure 4 · Final-result notification.

Three simple rules:

  • The evaluation_id is enough. You do not need to repeat the event type: the mechanism is the same for the whole catalog.
  • It is independent of the verification. A user can validate the token correctly and still not finish the change. That is why they are two different pieces of data.
  • It closes the learning loop. For each action, Koin stores the context, the strategy that was applied, and the final result. With that, it calibrates its decisions for your program.

If your system orchestrates the strategy

When Koin responds received and you run the verification yourself (see §11), you tell Koin how it went with the same PATCH endpoint. Koin combines that result with the rest of the signals and sends you the final decision (approved or denied) through callback_url.

{
  "type": "STATUS",
  "notification_date": "2026-10-16T14:10:44.307",
  "strategies": {
    "type": "VerificationCode",
    "results": [
      { "channel": "WhatsApp", "status": "Accept" }
    ]
  }
}

strategies.results[].status: Accept · Reject. The same mechanism is used for 3DS (CollectAuthRecovery), with provider and mode; the full example is in Strategies. Contract: Send Notifications.

10. Loyalty Pack: Account Protection for points programs

📘

A loyalty account is a wallet. Points are redeemed for tickets, transferred, and sold on parallel markets. The attacker is not after the account: they are after the balance.

To date, no account-protection provider publishes a data model for points programs. Koin models it with pieces that add to the account events you already know:

PieceWhat it capturesWhere it livesAPI
Points profileThe member's balance and tierSIGN_UP and transactions with pointsAccount Protection and transactional
Category (tier)Tier changes, by the member or by the systemCHANGE_PROFILE · CATEGORYAccount Protection
BeneficiariesWho can fly with the member's pointsCHANGE_PROFILE · BENEFICIARYAccount Protection
Transactions with pointsPoints purchase and purchase with points (transfer: under construction)POST /v1/antifraud/evaluationsTransactional

Anatomy of an account takeover in loyalty

Each step of the attack is an event Koin sees, and they are all linked by the same user_id:

sequenceDiagram
    actor Attacker
    participant Program as Loyalty program
    participant Koin
    Attacker->>Program: Login with leaked credentials
    Program->>Koin: LOGIN with a new device and an IP from another state
    Attacker->>Program: Changes the account email
    Program->>Koin: CHANGE_PROFILE EMAIL UPDATE
    Koin-->>Program: received with VerificationCode to the original channel
    Attacker->>Program: Adds an unknown beneficiary
    Program->>Koin: CHANGE_PROFILE BENEFICIARY ADD
    Attacker->>Program: Redeems the full balance for a ticket
    Program->>Koin: Transactional with Points payments and points_balance
    Koin-->>Program: denied because the redemption consumes 100 percent of the balance

Figure 5 · Anatomy of an account takeover in a loyalty program.

None of these actions is suspicious on its own. The sequence is, and Koin sees it in full because the two APIs share the member's identity.

10.1 Points profile at signup

New · SIGN_UP requires the same fields as the API Reference: user_id, first_name, last_name, birth_date, email, and profile. Inside profile, category and points_balance are required.

FieldTypeRequiredDescription
user_idstringYesMember identifier.
first_namestringYesFirst name.
last_namestringYesLast name.
birth_datestring (YYYY-MM-DD)YesDate of birth.
emailstringYesEmail contact.
profile.points_balanceintegerYesPoints balance at the time of the request.
profile.categorystringYesThe member's category or tier.
{
  "type": "SIGN_UP",
  "commerce": "company",
  "details": {
    "user_id": "company_8812345",
    "first_name": "Maria",
    "last_name": "Souza",
    "birth_date": "1988-03-14",
    "email": "[email protected]",
    "document": { "type": "CPF", "number": "12345678909", "nationality": "BR" },
    "authentication_type": "PASSWORD",
    "profile": {
      "points_balance": 0,
      "category": "SAFIRA"
    }
  },
  "device": { "ipv4": "187.61.115.149", "session_id": "455e1d2f-6db1-409a-8089-80e40cf37a12" }
}
📘

The balance is not updated with CHANGE_PROFILE. Koin receives the current balance on every transaction with points (see §10.4), right where it is used.

10.2 Category (tier) change

New · A tier change is reported with CHANGE_PROFILE, operation UPDATE on the CATEGORY field.

A category change is normally triggered by the program's system, not by the user. That is why CHANGE_PROFILE includes initiated_by, so Koin can tell an automatic change from one made by the member.

initiated_byWho started the change
USERThe member, from the app or the site.
SYSTEMThe customer's system (tier requalification, migration, support).
{
  "type": "CHANGE_PROFILE",
  "commerce": "company",
  "details": {
    "user_id": "company_8812345",
    "type": "UPDATE",
    "field": "CATEGORY",
    "old_value": "TOPAZIO",
    "new_value": "DIAMANTE",
    "initiated_by": "SYSTEM"
  }
}
📘

Why it matters

A tier upgrade changes the member's limits (redemptions, beneficiaries, cabin upgrades). A tier change started by the user, or right before a large redemption, is a signal Koin takes into account.

10.3 Beneficiaries

New · Beneficiaries are the people who can fly with the member's points. They are the natural destination of an account takeover: the attacker adds a third party and redeems in their name.

They are reported with CHANGE_PROFILE on the BENEFICIARY field. Each event carries one beneficiary. To add or remove several people, send one evaluation per person. There are three operations:

OperationWhenWhat you send
ADDA beneficiary is added.beneficiary: the new beneficiary.
DELETEA beneficiary is removed.beneficiary with only the document.
UPDATEA beneficiary's data is changed.old_beneficiary with the document of the beneficiary that changes, and new_beneficiary with their new data. Same old/new format as a phone change.

A full beneficiary (ADD, and new_beneficiary on UPDATE) always includes:

FieldTypeDescription
first_namestringFirst name.
last_namestringLast name.
birth_datestring (YYYY-MM-DD)Date of birth.
genderstringFEMALE · MALE · OTHER.
documentobjecttype and number (and optionally nationality). Examples: DNI, CPF, CNPJ, CUIL, CI, PASSPORT. PASSPORT is used for foreign beneficiaries.
emailstringThe beneficiary's email.
phoneobjectarea_code, number, and type, the same as in the rest of the API.
addressobjectAddress, with the same schema as the rest of the API (street, number, city, state, zip_code, country_code…).

On DELETE, beneficiary carries only the document. On UPDATE, old_beneficiary does the same: it is the key that identifies the beneficiary you are changing.

Add a beneficiary:

{
  "type": "CHANGE_PROFILE",
  "commerce": "company",
  "details": {
    "user_id": "company_8812345",
    "type": "ADD",
    "field": "BENEFICIARY",
    "initiated_by": "USER",
    "beneficiary": {
      "first_name": "João",
      "last_name": "Souza",
      "birth_date": "2012-07-02",
      "gender": "MALE",
      "document": { "type": "CPF", "number": "98765432100", "nationality": "BR" },
      "email": "[email protected]",
      "phone": { "area_code": "11", "number": "987650000", "type": "MOBILE" },
      "address": { "street": "Rua Augusta", "number": "1500", "city": "São Paulo", "state": "SP", "zip_code": "01304-001", "country_code": "BR" }
    }
  },
  "device": { "ipv4": "187.61.115.149", "session_id": "455e1d2f-6db1-409a-8089-80e40cf37a12" }
}

Remove a beneficiary:

{
  "type": "CHANGE_PROFILE",
  "commerce": "company",
  "details": {
    "user_id": "company_8812345",
    "type": "DELETE",
    "field": "BENEFICIARY",
    "initiated_by": "USER",
    "beneficiary": {
      "document": { "type": "CPF", "number": "98765432100" }
    }
  },
  "device": { "ipv4": "187.61.115.149", "session_id": "455e1d2f-6db1-409a-8089-80e40cf37a12" }
}

Update a beneficiary:

{
  "type": "CHANGE_PROFILE",
  "commerce": "company",
  "details": {
    "user_id": "company_8812345",
    "type": "UPDATE",
    "field": "BENEFICIARY",
    "initiated_by": "USER",
    "old_beneficiary": {
      "document": { "type": "CPF", "number": "98765432100" }
    },
    "new_beneficiary": {
      "first_name": "João",
      "last_name": "Souza",
      "birth_date": "2012-07-02",
      "gender": "MALE",
      "document": { "type": "CPF", "number": "98765432100", "nationality": "BR" },
      "email": "[email protected]",
      "phone": { "area_code": "21", "number": "912340000", "type": "MOBILE" },
      "address": { "street": "Rua Augusta", "number": "1500", "city": "São Paulo", "state": "SP", "zip_code": "01304-001", "country_code": "BR" }
    }
  }
}
📘

Why it matters

Koin builds the program's beneficiary network. The same beneficiary document showing up on many unrelated accounts is the fingerprint of a miles broker. And changing a beneficiary's email or phone is a way to divert redemption notifications.

10.4 Points transactions

Purchasing points and purchasing products with points are not evaluated with the Account Protection API: they are handled by the transactional API.

TransactionStatus
Points purchaseAvailable
Purchase with points (mixed payment)Available
Points transferUnder construction: the model will be published in a future version of this guide.
POST /v1/antifraud/evaluations

Full reference: Create Evaluation (Ecommerce).

📘

The golden rule: buyer.id is required

and it must carry the same identifier you send as user_id on Account Protection events. That is what lets Koin know which member is making the transaction and connect it with everything that happened on their account.

Points purchase

It is modeled with what already exists: a Generic item that indicates Points, with the quantity (quantity), the price (price), and the transaction total. The payment method is the usual one (card, Pix, and so on).

The item's seller_id indicates which loyalty program the points belong to (the airline, a bank, another coalition partner). That is how Koin distinguishes a purchase of the program's own points from a purchase of points from a partner program.

{
  "type": "Ecommerce",
  "buyer": {
    "id": "company_8812345",
    "first_name": "Maria",
    "last_name": "Souza",
    "email": "[email protected]",
    "document": { "type": "CPF", "number": "12345678909", "nationality": "BR" }
  },
  "items": [
    {
      "type": "Generic",
      "id": "PTS-10000",
      "name": "Purchase of 10,000 points",
      "category": { "id": "POINTS", "name": "Points" },
      "seller_id": "COMPANY",
      "quantity": 10000,
      "price": { "currency_code": "BRL", "value": 0.07 }
    }
  ],
  "payments": [
    {
      "method": "CreditCard",
      "amount": { "currency_code": "BRL", "value": 700.00 },
      "installments": 1,
      "details": { "bin": "424242", "last_digits": "4444", "brand_name": "Visa" },
      "payer": { "first_name": "Maria", "last_name": "Souza", "email": "[email protected]" }
    }
  ],
  "transaction": {
    "reference_id": "COMPANY-PTS-000123",
    "country_code": "BR",
    "total_amount": { "currency_code": "BRL", "value": 700.00 }
  },
  "device": { "ipv4": "187.61.115.149", "session_id": "455e1d2f-6db1-409a-8089-80e40cf37a12" }
}

Purchase with points and mixed payment

Mixed payment methods are supported: in payments you send one method for each part of the payment, for example CreditCard for the cash portion and Points for the points used (quantity) with their equivalent amount.

In addition, the buyer includes their points profile in buyer.profile:

FieldRequiredDescription
buyer.profile.points_balanceYesPoints balance before the transaction.
buyer.profile.categoryNoThe member's category or tier.
{
  "type": "Ecommerce",
  "buyer": {
    "id": "company_8812345",
    "first_name": "Maria",
    "last_name": "Souza",
    "email": "[email protected]",
    "document": { "type": "CPF", "number": "12345678909", "nationality": "BR" },
    "profile": {
      "points_balance": 48000,
      "category": "DIAMANTE"
    }
  },
  "items": [
    {
      "type": "Flight",
      "id": "AD4050-2026-12-20",
      "quantity": 1,
      "price": { "currency_code": "BRL", "value": 1200.00 },
      "itinerary_type": "ONEWAY",
      "route_type": "DOMESTIC",
      "routes": [
        {
          "position": 1,
          "segments": [
            {
              "position": 1,
              "airline_code": "AD",
              "flight_number": 4050,
              "departure": { "airport_code": "VCP", "date": "2026-12-20T08:15:00Z" },
              "arrival": { "airport_code": "REC", "date": "2026-12-20T11:20:00Z" }
            }
          ]
        }
      ],
      "passengers": [
        {
          "first_name": "João",
          "last_name": "Souza",
          "birth_date": "2012-07-02",
          "document": { "type": "CPF", "number": "98765432100", "nationality": "BR" }
        }
      ]
    }
  ],
  "payments": [
    {
      "method": "Points",
      "quantity": 30000,
      "amount": { "currency_code": "BRL", "value": 850.00 }
    },
    {
      "method": "CreditCard",
      "amount": { "currency_code": "BRL", "value": 350.00 },
      "installments": 1,
      "details": { "bin": "424242", "last_digits": "4444", "brand_name": "Visa" },
      "payer": { "first_name": "Maria", "last_name": "Souza", "email": "[email protected]" }
    }
  ],
  "transaction": {
    "reference_id": "COMPANY-RES-778812",
    "country_code": "BR",
    "total_amount": { "currency_code": "BRL", "value": 1200.00 }
  },
  "device": { "ipv4": "187.61.115.149", "session_id": "455e1d2f-6db1-409a-8089-80e40cf37a12" }
}
📘

Why the prior balance matters

With points_balance and quantity, Koin knows what share of the balance is being spent. Draining 100% of an account built up over years, from a new device, is the clearest signature of a taken-over account.

The Flight item (routes, segments, passengers with a document) is already part of the transactional model. With it, Koin cross-checks the redemption passengers against the account beneficiaries. See the item types in the transactional API Reference.

11. Operating modes

Each customer chooses how they operate, from the dashboard and without changing the API contract:

Koin operatesThe customer operates
StrategiesThe response includes a link to Koin's hosted page. The user verifies there and Koin sends you the final evaluation decision (approved or denied) through callback_url.The response includes the recommended type. You run your validation, report its result through PATCH, and Koin sends you the final decision through callback_url.
CommunicationsKoin sends the token and the magic link on the customer's behalf, by email or WhatsApp. Koin does not send SMS.You send the messages through your own channels.
📘

You can start with your current verifications and migrate to Koin's strategy by strategy.

12. Errors and best practices

Response codes

CodeMeaningWhat to do
200Evaluation created.Apply the decision.
400Validation error.Fix the request: causes indicates which field failed.
401Unauthorized.Check the key and the endpoint permissions.
404Evaluation not found (PATCH).Check the evaluation_id.
412The case is already closed (PATCH with type: STATUS).Do not retry. The evaluation is no longer open.
500Internal error.Retry with backoff and apply your fallback policy.

Validation error example:

{
  "code": 400,
  "message": "Validation errors",
  "causes": ["type must not be blank"]
}

Integration checklist

  • The same user_id everywhere: Account Protection and transactional (buyer.id).
  • session_id on every event: integrate the device SDK on web and app.
  • Also send what fails: logins with client_result: FAILED and abandoned actions.
  • Close every cycle: PATCH with the final result of each evaluation.
  • Listen on callback_url: that is where the final decision arrives when there was a strategy.
  • Define your fallback: what your flow does if Koin does not respond in time (for example, allow the login and ask for a token on high-value actions).
  • Explicit timeouts: set a timeout on your HTTP client and record fallback cases.
  • Do not reuse evaluations: one action, one evaluation.

13. From sandbox to production

flowchart LR
    A["1. Sandbox<br/>integration and tests"]
    B["2. Listener mode<br/>real data, without blocking"]
    C["3. Customization<br/>optional, per program"]
    D["4. Go-live<br/>active decisions"]
    E["5. Continuous improvement<br/>final results and<br/>confirmed fraud"]
    A --> B
    B --> C
    C --> D
    D --> E
    classDef go fill:#10B132,color:#06210F,stroke:#10B132
    class D go
    classDef decision fill:#E6F6EA,color:#0A7F25,stroke:#10B132
    class E decision

Figure 6 · From sandbox to production.

  1. Sandbox. You integrate against https://api-sandbox.koin.com.br with your test key.
  2. Listener mode. Koin receives real traffic and learns your program's behavior without affecting your members. On the transactional API it is enabled with transaction.listener_mode: true: the evaluation is recorded and always returns approved.
  3. Customization (optional). Koin already decides on its own. If you want, you adjust rules, lists, thresholds, and strategies per event with our team.
  4. Go-live. You turn decisions on, event by event.
  5. Continuous improvement. Final results and confirmed fraud cases feed the engine back.

Let's talk?

The Koin team supports every integration, from sandbox to go-live. Full API Reference: api-docs.koin.com.br