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
- Sandbox — api-sandbox.koin.com.br
- API Reference · Account Protection — Create Evaluation · Notifications
- API Reference · Transactional — Create Evaluation (Ecommerce)
- For agents — llms.txt
NewMarks 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:
| Decision | What it means | What you do |
|---|---|---|
approved | No risk signals | You continue without friction |
received | A verification is required | You show the strategy Koin indicates (token, liveness, 3DS…) |
denied | High risk or blocked account | You 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
| Koin | What is typical in the market | |
|---|---|---|
| Decides on its own, and can be customized | Koin 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 verification | Koin 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 loyalty | Points 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 LatAm | Native 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 APIs | The 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 documentation | Public, 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/jsonStep 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 mattersThe 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.
| Concept | In one line | Field |
|---|---|---|
| Event | The sensitive action you send to Koin so it can evaluate it. | type |
| Verification strategy | The 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 context | The 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 customer | The 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 result | How 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_resulthappens 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:approvedordenied.
6. Decisions and strategies
The three decisions
status | Meaning | What your system does |
|---|---|---|
approved | The action may continue. | Completes the action. |
received | In 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. |
denied | Block. | 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),receivedalways comes with a strategy the user resolves in the moment. It never leaves the user waiting for a manual review.
Available strategies
strategies[].type | What it validates | When it is typically used |
|---|---|---|
VerificationCode | 6-digit token by email or WhatsApp, or magic link. | Forgotten password, contact change, account closure. |
Liveness | Facial biometrics with a proof of life. | High risk: contact change after a suspicious login. |
CollectAuthRecovery | 3DS2 authentication (FRICTIONLESS or CHALLENGE). | Actions that involve a card. |
ManualVerification | Review 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.
| Event | When you send it | When you do not | Reference verification |
|---|---|---|---|
LOGIN | The 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_UP | Account 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_PASSWORD | Any 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_PROFILE | A 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_ACCOUNT | The 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
| Field | Required | Why it matters |
|---|---|---|
type | Yes | Defines the event and the details schema. |
details.user_id | Yes | The identity that links every account event, in Account Protection and in the transactional API. Always use the same identifier. |
commerce | Recommended | Identifies the merchant or program the event comes from. It lets you separate decisions and reports by brand or channel. |
device.session_id | Recommended | Generated by the device SDK. Without it, Koin loses the strongest signal against account takeover. |
device.ipv4 / device.ipv6 | Recommended (one of the two) | Geolocation, ASN, and detection of distributed attacks. |
device.merchant_device | Optional | Your own device identifier, so you can cross-check it with Koin's. |
callback_url | Optional | HTTPS endpoint where you receive the final evaluation decision when there was a strategy. |
sla_date | Optional | How 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.
authentication | Use case | Reference verification |
|---|---|---|
unauthenticated | I forgot my password. The user, without a session, starts the process. The previous password stays valid until it finishes. | Yes (for example, a token). |
authenticated | Change from the profile. The user, with an active session, changes their password. | Typically no. |
system | Forced 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 → | ADD | UPDATE | DELETE | CONFIRM |
|---|---|---|---|---|
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" }
}- The user asks to close their account.
- You send
CLOSE_ACCOUNT. - Koin responds
receivedwith the strategy (token). - The user validates the token, Koin sends you the final decision, and you execute the closure.
- 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_idis 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:
| Piece | What it captures | Where it lives | API |
|---|---|---|---|
| Points profile | The member's balance and tier | SIGN_UP and transactions with points | Account Protection and transactional |
| Category (tier) | Tier changes, by the member or by the system | CHANGE_PROFILE · CATEGORY | Account Protection |
| Beneficiaries | Who can fly with the member's points | CHANGE_PROFILE · BENEFICIARY | Account Protection |
| Transactions with points | Points purchase and purchase with points (transfer: under construction) | POST /v1/antifraud/evaluations | Transactional |
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.
| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | Member identifier. |
first_name | string | Yes | First name. |
last_name | string | Yes | Last name. |
birth_date | string (YYYY-MM-DD) | Yes | Date of birth. |
email | string | Yes | Email contact. |
profile.points_balance | integer | Yes | Points balance at the time of the request. |
profile.category | string | Yes | The 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_by | Who started the change |
|---|---|
USER | The member, from the app or the site. |
SYSTEM | The 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 mattersA 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:
| Operation | When | What you send |
|---|---|---|
ADD | A beneficiary is added. | beneficiary: the new beneficiary. |
DELETE | A beneficiary is removed. | beneficiary with only the document. |
UPDATE | A 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:
| Field | Type | Description |
|---|---|---|
first_name | string | First name. |
last_name | string | Last name. |
birth_date | string (YYYY-MM-DD) | Date of birth. |
gender | string | FEMALE · MALE · OTHER. |
document | object | type and number (and optionally nationality). Examples: DNI, CPF, CNPJ, CUIL, CI, PASSPORT. PASSPORT is used for foreign beneficiaries. |
email | string | The beneficiary's email. |
phone | object | area_code, number, and type, the same as in the rest of the API. |
address | object | Address, 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 mattersKoin 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.
| Transaction | Status |
|---|---|
| Points purchase | Available |
| Purchase with points (mixed payment) | Available |
| Points transfer | Under construction: the model will be published in a future version of this guide. |
POST /v1/antifraud/evaluationsFull reference: Create Evaluation (Ecommerce).
The golden rule:buyer.idis requiredand it must carry the same identifier you send as
user_idon 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:
| Field | Required | Description |
|---|---|---|
buyer.profile.points_balance | Yes | Points balance before the transaction. |
buyer.profile.category | No | The 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 mattersWith
points_balanceandquantity, 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 operates | The customer operates | |
|---|---|---|
| Strategies | The 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. |
| Communications | Koin 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
| Code | Meaning | What to do |
|---|---|---|
200 | Evaluation created. | Apply the decision. |
400 | Validation error. | Fix the request: causes indicates which field failed. |
401 | Unauthorized. | Check the key and the endpoint permissions. |
404 | Evaluation not found (PATCH). | Check the evaluation_id. |
412 | The case is already closed (PATCH with type: STATUS). | Do not retry. The evaluation is no longer open. |
500 | Internal 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_ideverywhere: Account Protection and transactional (buyer.id). -
session_idon every event: integrate the device SDK on web and app. - Also send what fails: logins with
client_result: FAILEDand abandoned actions. - Close every cycle:
PATCHwith 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.
- Sandbox. You integrate against
https://api-sandbox.koin.com.brwith your test key. - 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. - Customization (optional). Koin already decides on its own. If you want, you adjust rules, lists, thresholds, and strategies per event with our team.
- Go-live. You turn decisions on, event by event.
- 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
Updated 2 days ago
