# In-store payment: Android Intent API

## Overview

The Android Intent API is for POS applications running on the terminal itself (or on the same Android device as Yavin Pay). Your app sends a deep link Intent to Yavin Pay and receives the result in onActivityResult. Choose it when the POS and the payment application share the same device.

## At a glance

|  |  |
| --- | --- |
| Base URL | Deep link scheme: yavin://com.yavin.macewindu/v4/<action>?data=$queryParams |
| Authentication | Merchant login in Yavin Pay with [My Yavin](https://my.yavin.com/) credentials |
| Naming convention | camelCase for all fields, except the entries of tax_breakdown which are snake_case |
| Current version | v4 |
| Result delivery | Synchronous: returned to your activity via onActivityResult, in the response extra (JSON) and, on error, the message extra |

## Endpoints

| Action | Deep link | Purpose |
| --- | --- | --- |
| Payment | /v4/payment | Start a debit or refund transaction |
| Print | /v4/print | Print free content |
| Share receipt | /v4/share-receipt | Share a receipt by SMS, email or print |
| Transactions | /v4/transactions | Fetch the transaction history |
| Reversal | /v4/reversal | Reverse a recent transaction (less than 16 hours old) |
| NFC reader | /v4/nfc-reader | Read an NFC tag via Yavin Pay |

### Versions

| API version | Last updated |
| --- | --- |
| v4 | 22/08/2024 |
| v1 | 02/05/2022 |

## Before you start

Payload encoding. The request is a JSON object serialized then URI-encoded into the data query parameter of the deep link.

Request code. When calling startActivityForResult, define your own request code (any integer, eg 8888). It is your reference to match the asynchronous response with the request, similar to a webhook correlation ID.

Reading the response. The result JSON is in the response extra. When an error message exists, it is in a separate message extra, never inside the response JSON. The response extra can be absent (for example when the request is rejected before any transaction is created, or when data itself is null): read the extras defensively and never pass a null string to your JSON parser.

```kotlin
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode != REQUEST_CODE_PAYMENT) return

    // Both extras are optional: data can be null (eg resultCode == RESULT_CANCELED)
    val json: String? = data?.getStringExtra("response")
    val message: String? = data?.getStringExtra("message")

    if (json == null) {
        // No transaction was returned: treat the attempt as failed
        onPaymentFailed(message ?: "No response from Yavin Pay (resultCode=$resultCode)")
        return
    }

    val response = Gson().fromJson(json, TransactionResponse::class.java)
    if (response.status == "ok") {
        onPaymentSucceeded(response)
    } else {
        // A declined payment carries no message in the JSON: use the message extra if any
        onPaymentFailed(message ?: "Payment declined")
    }
}
```

Responses. The payment actions return a Transaction object, documented at the end of this page along with the other shared objects, with a status field: ok on success, ko on failure. Keys whose value is null are omitted: tolerate missing keys. The transactions action has no status field (see Fetch transactions).

> ⛔ Never send a negative amount. On the Android Intent API, the terminal does not reject a negative amount or giftAmount: it uses its absolute value. An amount of -1000 is processed as a 10,00 € payment. Validate amounts on the POS side before sending the Intent.

Idempotency. Send an idempotentUuid on every payment request, one per payment attempt. See [IdempotentUuid Management](https://app.notion.com/p/3bc9a8f4fd9a81d6a3cbfce4939ac0e9).

Currency. The currency is the one configured on the merchant profile in the Yavin database.

---

## Endpoints

### Start a debit transaction

Starts a debit payment on the terminal.

#### Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `amount` | Integer | yes |  | Amount in cents, must be greater than 0. Never send a negative value: it is not rejected and its absolute value is charged |
| `transactionType` | String | no | debit | Use debit. Values are strict (debit or refund): any other value, credit included, is silently processed as a debit |
| `idempotentUuid` | String | no | autogenerated | Unique identifier of the payment attempt, see IdempotentUuid Management. A ko transaction is returned as is: generate a new UUID to retry |
| `customer` | Customer | no |  | Pre-filled customer info for receipt sharing |
| `enableGiftScreen` | Boolean | no | null | true: show the tips screen. false: skip it |
| `giftAmount` | Integer | no | 0 | Tip or donation in cents |
| `receiptTicket` | ReceiptTicket | no |  | Receipt printed with the card ticket |
| `receiptTicketJson` | String | no | null | Additional JSON payload as string |
| `reference` | String | no | null | Waiter or person processing the transaction |
| `vendor` | Vendor | no |  | Your software name and version |
| `checkoutExternalId` | String | no | null | Merchant-unique checkout ID, required when any external order field below is sent. It must be unique: a request is rejected if a completed ok transaction already uses this value |
| `externalTableNumber` | String | no | null | Table number. Requires checkoutExternalId |
| `externalOrderNumber` | String | no | null | Human-readable order number. Requires checkoutExternalId |
| `externalOrderId` | String | no | null | Unique technical order ID from your POS, for reconciliation. Requires checkoutExternalId |

#### Request

_Kotlin_
```kotlin
val request = TransactionRequest(
  amount = 100,
  transactionType = "debit",
  idempotentUuid = "2f1c2e2a-6c21-4a4b-9c2b-9f6f2c8e9b51",
  customer = Customer("John", "Doe", "john@yavin.com"),
  vendor = Vendor("Awesome Partner", "1.2.3"),
  receiptTicket = ReceiptTicket(data = "This is a wonderful\n receipt ticket to print", format = "text"),
  receiptTicketJson = JSONObject("{\"transactionId\": \"123456\", \"amount\": 3500 }").toString(),
  checkoutExternalId = "order-123",
  externalTableNumber = "12",
  externalOrderNumber = "A-123",
  externalOrderId = "order-123"
)

val jsonData = Gson().toJson(request)
val queryParams = Uri.encode(jsonData)

val intent = Intent(Intent.ACTION_VIEW).apply {
  data = Uri.parse("yavin://com.yavin.macewindu/v4/payment?data=$queryParams")
}

startActivityForResult(intent, REQUEST_CODE_PAYMENT)
```

#### Response

On success, the response is a Transaction object with status = ok and a transactionId; the request fields (reference, customer, idempotentUuid, checkoutExternalId and the external order fields) are echoed back.

If a transaction already exists for this idempotentUuid, it is returned as is, including when it is still in progress: in that case it has no status field.

On failure, the response extra contains a Transaction object with status = ko and no message. When there is an error message, it is in the separate message extra. When the request is rejected before a transaction is created, the response extra is absent and only message is set. Possible messages:

- Error: refund need to be activated contact support: refunds are not enabled for this merchant

- Transaction cannot proceed: unknown payment gateway or amount is not greater than 0

- Error: amount exceeds the maximum authorized credit amount (N): refund above the maximum authorized amount N

- Error: checkoutExternalId must not be null if any of the following values are provided: externalOrderId, externalOrderNumber, externalTableNumber, or items: an external order field was sent without checkoutExternalId

- Error: checkoutExternalId: The checkoutExternalId is already in use. Please ensure you provide a unique value: a completed ok transaction already uses this checkoutExternalId

- A request is already in progress: another request is in progress

- A timeout message when the optional screens (tips, reference, review) exceed 60 seconds

- Unknown Error or an exception message: Android internal error

---

### Start a refund transaction

Credits funds back to the customer card. Same deep link and payload as debit, with transactionType set to refund.

#### Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `transactionType` | String | yes | debit | Must be set to refund. Any other value, such as credit, starts a debit |

#### Response

Same as the debit action: a Transaction object with transactionType = refund on success, status = ko with the same message list on failure.

---

### Print a receipt ticket

Prints free content, an invoice for example.

#### Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `data` | String | yes | null | Content to print |
| `format` | String | no | text | text or escpos |

#### Request

_Kotlin_
```kotlin
val request = PrintRequest(format = "text", data = "Text to print")
val queryParams = Uri.encode(Gson().toJson(request))
val intent = Intent(Intent.ACTION_VIEW).apply {
  data = Uri.parse("yavin://com.yavin.macewindu/v4/print?data=$queryParams")
}
startActivityForResult(intent, REQUEST_CODE_PRINT)
```

#### Response

status = ok means the print request was accepted, not that something was printed: the content is not validated, so an empty data can also return ok. When the data query parameter of the deep link is missing or cannot be decoded, status = ko with the message Error: bad request print.

---

### Share a receipt ticket

Shares a receipt by SMS, email or print. If customer is provided the receipt is sent directly; otherwise the terminal prompts for the missing information.

#### Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `receiptTicket` | ReceiptTicket | yes |  | Receipt to share |
| `transactionId` | String | yes |  | Transaction linked to the receipt |
| `medium` | String | yes |  | One of yavin, sms, email, print. With yavin, the receipt is stored and shared according to the user notification preferences |
| `customer` | Customer | no |  | Recipient info for SMS or email |

#### Request

_Kotlin_
```kotlin
val request = ShareRequest(
  receiptTicket = ReceiptTicket(format = "text", data = "Text to print"),
  transactionId = "gks15fQSfw",
  medium = "email",
  customer = Customer("John", "Doe", email = "john@yavin.com")
)
val queryParams = Uri.encode(Gson().toJson(request))
val intent = Intent(Intent.ACTION_VIEW).apply {
  data = Uri.parse("yavin://com.yavin.macewindu/v4/share-receipt?data=$queryParams")
}
startActivityForResult(intent, REQUEST_CODE_SHARE)
```

#### Response

status = ok when the receipt was shared. On failure, status = ko with one of:

- Error: transaction with id ${transactionId} not found on the terminal

- Error: phone number must follow the international format starting with + symbol

- Error: wrong parameter 'medium' : possible values are 'yavin', 'print', 'sms' or 'email': medium missing or invalid

- Error: wrong deeplink input data: badly formatted request

- Error: transaction is missing: transactionId missing

---

### Fetch transactions

Retrieves the transactions of the terminal. Use limit and offset for pagination. If no date filters are set, the last 30 days are fetched.

#### Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `startDate` | String | no | last 30 days | Earliest date to include, ISO 8601 |
| `endDate` | String | no | today | Latest date to include, ISO 8601 |
| `startTime` | String | no | 00:00:00Z | Start time on startDate, ISO 8601 |
| `endTime` | String | no | 23:59:59Z | End time on endDate, ISO 8601 |
| `limit` | Integer | no | 50 | Max transactions returned, up to 200 |
| `offset` | Integer | no | 0 | Pagination offset |

#### Request

_Kotlin_
```kotlin
val request = TransactionsRequest(
  startDate = "2022-01-01",
  endDate = "2022-02-01",
  startTime = "00:00:00",
  endTime = "23:59:59",
  limit = 50
)
val queryParams = Uri.encode(Gson().toJson(request))
val intent = Intent(Intent.ACTION_VIEW).apply {
  data = Uri.parse("yavin://com.yavin.macewindu/v4/transactions?data=$queryParams")
}
startActivityForResult(intent, REQUEST_CODE_TRANSACTIONS)
```

#### Response

Returns total, count, limit, offset, and transactions. There is no status field.

- total is the number of transactions in this response (equal to count), not the total number of matching transactions. To paginate, increase offset by limit until a page returns fewer than limit transactions.

- Each item of transactions has the fields of ItemTransactionResponse above. It is a reduced object, not the full Transaction object: it has no cardToken, paymentApplication, tickets or idempotentUuid.

- createdAt is the terminal timestamp of the transaction, in ISO 8601 UTC.

> ⚠️ If data is missing or invalid, no response is returned and the Yavin Pay screen stays open. Validate the request on the POS side before sending it.

---

### Reverse a transaction

Reverses a recent transaction.

> ⚠️ Reversal conditions. The original transaction is an ok debit processed on the same terminal issuing the reversal, less than 16 hours ago (terminal time), and the reversal amount is equal to the original total amount, tip included (amount + giftAmount).

#### Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `amount` | Integer | yes | 0 | Amount in cents, must equal the original total amount (amount  • giftAmount) |
| `initialTransactionId` | String | yes | null | Transaction to reverse |

#### Response

status = ok with reversalTransactionId (the new reversal transaction) and initialTransactionId (the reversed one). On failure, status = ko with one of:

- Transaction ID is mandatory: initialTransactionId missing

- Transaction not found for ID: <transactionId>: unknown transaction on this terminal

- Amount mismatch: amount differs from the original total amount (amount + giftAmount)

- Transaction has already been reversed

- Cannot reverse this Transaction or time window expired: the transaction is not an ok debit or VAD transaction, or is more than 16 hours old

- No suitable gateway found to handle reversal

- An unexpected error occurred. Please try again later.: generic error

> ⚠️ Swapped fields on error. In an error response, reversalTransactionId contains the ID of the initial transaction, and initialTransactionId contains the ID of the reversal attempt. Read them accordingly.

---

### Read NFC tag via Pay

Reads an NFC tag through Yavin Pay.

#### Parameters

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `readerIncentive` | String | no |  | Text displayed while waiting for the tag |
| `timeout` | Long | no | 10000 | Read timeout in milliseconds, maximum 10000: a higher value is capped at 10000 |

#### Request

_Kotlin_
```kotlin
val request = NFCReaderRequestV4(10000, "Approchez votre carte")
val queryParams = Uri.encode(Gson().toJson(request))
val intent = Intent(Intent.ACTION_VIEW).apply {
  data = Uri.parse("yavin://com.yavin.macewindu/v4/nfc-reader?data=$queryParams")
}
startActivityForResult(intent, REQUEST_CODE_READ)
```

#### Response

status is a boolean: true with a TagInfo object when the tag was read, false otherwise.

---

## Other

### The Transaction object

Returned by the payment actions. Fields coming from your request are echoed back as is. Keys whose value is null are omitted: tolerate missing keys. A declined payment has status = ko and no message (see Reading the response).

```json
{
  "status": "ok",
  "transactionId": "xPUyi4fmdibD",
  "amount": 1000,
  "giftAmount": 0,
  "currencyCode": "EUR",
  "transactionType": "debit",
  "appVersion": "3.2.8",
  "cardToken": "1234567890",
  "clientCardTicket": "...",
  "merchantCardTicket": "...",
  "paymentApplication": "EMV_CONTACTLESS",
  "scheme": "CB",
  "issuer": "VISA",
  "idempotentUuid": "2f1c2e2a-6c21-4a4b-9c2b-9f6f2c8e9b51",
  "cartId": "ORDER-2026-000123",
  "reference": "Luke",
  "customer": {
    "firstName": "John",
    "lastName": "Doe",
    "email": "john@yavin.com"
  },
  "checkoutExternalId": "order-123",
  "externalTableNumber": "12",
  "externalOrderNumber": "A-123",
  "externalOrderId": "order-123"
}
```

#### Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| status | String | ok or ko. Absent on a transaction still in progress |
| transactionId | String | Server-side identifier |
| amount | Integer | Amount in cents, excluding giftAmount |
| giftAmount | Integer | Tip or donation in cents |
| currencyCode | String | ISO 4217 code |
| transactionType | String | Lowercase: debit or refund. Tolerate other values (eg na) |
| appVersion | String | Yavin Pay app version |
| cardToken | String | Unique token of the customer card |
| clientCardTicket / merchantCardTicket | String | Customer and merchant card tickets |
| paymentApplication | String | eg EMV_CONTACTLESS |
| scheme | String | Acceptance network (eg VISA) |
| issuer | String | Card issuer |
| idempotentUuid | String | UUID from the request |
| reference, customer |  | Echoed from the request |
| checkoutExternalId, externalTableNumber, externalOrderNumber, externalOrderId | String | Echoed from the request |

---

### The Customer object

Optional on the payment and share-receipt actions. Pre-fills the customer information so the receipt can be sent by SMS or email without prompting on the terminal.

```json
{
  "customer": {
    "firstName": "John",
    "lastName": "Doe",
    "phone": "+33612345678",
    "email": "john@yavin.com"
  }
}
```

#### Attributes

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| firstName | String | no | First name |
| lastName | String | no | Last name |
| email | String | no | Email |
| phone | String | no | International format starting with + |
| birthDate | String | no | Birthdate |

---

### The ReceiptTicket object

Content printed alongside the card ticket on the payment action, or shared on the share-receipt action.

```json
{
  "receiptTicket": {
    "data": "Receipt ticket here to print if needed",
    "format": "text"
  }
}
```

#### Attributes

| Attribute | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| data | String | yes |  | Content to print |
| format | String | no | text | Format of the content |
| tax_breakdown | Array | no |  | Array of Tax objects, one per tax rate |

---

### The Tax object

Optional breakdown attached to a ReceiptTicket, under the tax_breakdown key, with one entry per tax rate.

> ⚠️ The key is tax_breakdown (not tax), and its entries are in snake_case.

```json
{
  "tax_breakdown": [
    { "tax_amount": 167, "tax_percentage": 20 },
    { "tax_amount": 50, "tax_percentage": 10 }
  ]
}
```

#### Attributes

| Attribute | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| tax_amount | Integer | no | null | Calculated tax amount in cents |
| tax_percentage | Integer | no | null | Percentage applied to the amount (eg 20 for 20%) |

---

### The Vendor object

Identifies your POS software. Optional everywhere, but strongly recommended: it is what lets our support team trace an issue back to a specific integration and version.

```json
{
  "vendor": {
    "softwareName": "MyPOS",
    "softwareVersion": "1.0"
  }
}
```

#### Attributes

| Attribute | Type | Required | Description |
| --- | --- | --- | --- |
| softwareName | String | no | Name of the POS software |
| softwareVersion | String | no | Version of the POS software |

---

### The TagInfo object

Returned by the NFC reader action, describing the tag that was read.

```json
{
  "tagInfo": {
    "serialNumber": "04A2B3C4D5E6F7"
  }
}
```

#### Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| serialNumber | String | Tag identifier |

---

### Related pages

[IdempotentUuid Management](https://app.notion.com/p/3bc9a8f4fd9a81d6a3cbfce4939ac0e9)
