---
description: >-
  Create, track, and manage remittance transactions — execute cross-border transfers, handle 3D Secure, monitor status transitions, and cancel or reverse transactions.
---

# Transaction

The Transaction endpoint is the culmination of the remittance flow. It links together the sender, recipient, funding source, recipient account, and quote to execute a cross-border payment.

***

### Creating a Transaction

**Endpoint:** `POST /organizations/{tenant}/fx/transactions`\
**Authentication:** Agent-level (`x-api-key` + `x-agent-id` + `x-agent-api-key`)

#### Request Body

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `senderId` | string (UUID) | Yes | The participant ID of the sender (from `POST /people`) |
| `recipientId` | string (UUID) | Yes | The participant ID of the recipient (from `POST /people`) |
| `fundingAccountId` | string (UUID) | Yes | The sender's funding source (from `POST /fundingAccounts`) |
| `recipientAccountId` | string (UUID) | Yes | The recipient's payout account (from `POST /recipientAccounts/gateway`) |
| `quoteId` | string (UUID) | Yes | The locked FX quote (from `POST /payout/quotes`) |
| `externalId` | string | No | Your internal reference ID for reconciliation. Returned in webhooks as `externalTransactionId`. |
| `deviceData` | object | Conditional | Device intelligence for fraud prevention (see below) |
| `deviceData.userIpAddress` | string | Conditional | The end user's IP address. **Required** when fingerprinting is enabled for your tenant (the default). |
| `deviceData.fingerprint` | string | Conditional | The `requestId` from the device fingerprint library. **Required** when fingerprinting is enabled (the default). |
| `deviceData.visitor` | string | Recommended | The `visitorId` from the device fingerprint library |
| `additionalData` | object | No | Legacy location for `fingerprint` and `visitor` (both accepted), plus country-specific fields per the [transaction schema](data-population/transaction-schema.md) |
| `sendingReason` | string | No | Purpose of the transfer (e.g., "Family support", "Payment for services") |
| `senderReceiverRelationship` | string | No | Relationship between sender and recipient (e.g., "Family member", "Business partner") |
| `sourceOfFunds` | string | No | Origin of funds (e.g., "Salary", "Savings", "Business income") |

#### Device Fingerprint

Most tenants have device fingerprinting enabled (it's the platform default). When enabled, every transaction must include a fingerprint and the user's IP address, or the request is rejected. Integrate a device fingerprint library into your client application — Inyo provides the API keys for the fingerprint service during onboarding.

Before submitting a transaction, call the fingerprint library on the client side and pass the results:

* `deviceData.fingerprint` — the `requestId` from the fingerprint library
* `deviceData.visitor` — the `visitorId` from the fingerprint library
* `deviceData.userIpAddress` — the end user's IP address

> **Send BOTH `fingerprint` and `visitor`.** The payment gateway's server-side device-intelligence integration only runs when both values are present. Sending only one silently drops the device-intelligence block from the gateway call, which reduces fraud-scoring accuracy and can contribute to declines.

> `fingerprint` and `visitor` are also accepted inside `additionalData` (legacy format). `deviceData` is the preferred location.

#### Sample Request

```bash
curl --request POST \
  --url https://{FQDN}/organizations/$TENANT/fx/transactions \
  --header 'Content-Type: application/json' \
  --header "x-api-key: $API_KEY" \
  --header "x-agent-id: $AGENT_ID" \
  --header "x-agent-api-key: $AGENT_KEY" \
  --data '{
  "externalId": "your-internal-reference-123",
  "senderId": "422a55b2-78c3-4509-8654-16e7375d3e40",
  "recipientId": "4b9dcb4e-2e2a-4e96-b2a9-fcd868b2f4ed",
  "fundingAccountId": "3fe5f23f-65d3-41b6-b34c-33db3c4d8ef9",
  "recipientAccountId": "67203d69-dc48-41d4-b2ac-52682d39b032",
  "quoteId": "30bf05d2-b416-4e71-b8e8-1ef158a2b414",
  "deviceData": {
    "userIpAddress": "200.123.131.112",
    "fingerprint": "1727364523875.mchLfB",
    "visitor": "jkS9xp4FHlqOSfGR"
  },
  "sendingReason": "Family support",
  "senderReceiverRelationship": "Family member",
  "sourceOfFunds": "Salary"
}'
```

#### Sample Response (HTTP 202)

```json
{
  "id": "9035ee18-052b-4176-a940-ccfe599a1829",
  "sender": {
    "id": "ce06773d-6472-4c93-bdf5-8eb68ab01c7f",
    "type": "PERSON",
    "name": "Rob Dalas",
    "phoneNumber": "+1 123 456435"
  },
  "recipient": {
    "id": "156675d5-1d1b-4b4d-997e-5c15ae129b82",
    "type": "PERSON",
    "name": "Bob Danilo",
    "phoneNumber": "+55 71 91234-5678"
  },
  "tenantId": "master",
  "externalId": "your-internal-reference-123",
  "agentId": "41f55211-1eea-4747-9a03-68e99f07ede5",
  "createdAt": "2025-05-29T17:26:27.264312",
  "anchorCurrencyAmount": { "amount": "102.53", "currency": "USD" },
  "complianceStatus": "Pending",
  "payoutStatus": "Pending",
  "paymentStatus": null,
  "quoteId": "af2d55d8-1d40-426a-8d4a-7454ccdcbe8a",
  "fundingAccountId": "61bc14a1-b3a2-40b4-b395-0496e4f554a1",
  "recipientAccountId": "4fbc57a6-f673-44ba-88aa-c6e71eb9181b",
  "exchangeRate": "5.571062480000000",
  "totalAmount": { "amount": "102.53", "currency": "USD" },
  "receivingAmount": { "amount": "571.20", "currency": "BRL" },
  "fee": { "amount": "1.54", "currency": "USD" },
  "conversionAmount": { "amount": "102.53", "currency": "USD" },
  "type": "FX",
  "senderId": "ce06773d-6472-4c93-bdf5-8eb68ab01c7f",
  "recipientId": "156675d5-1d1b-4b4d-997e-5c15ae129b82",
  "sendingReason": "Family support",
  "receipt": { "..." : "regulatory disclosures" }
}
```

#### Response Fields

| Field | Description |
| ----- | ----------- |
| `id` | Unique transaction ID |
| `complianceStatus` | Current compliance screening result (see lifecycle below) |
| `payoutStatus` | Current payout delivery status (see lifecycle below) |
| `paymentStatus` | `ActionRequired` when the card requires a 3DS challenge, otherwise `null` |
| `redirectAcsUrl` | 3DS challenge URL — present only when `paymentStatus` is `ActionRequired` |
| `status.messages` | Audit trail of status transitions with timestamps |
| `exchangeRate` | The applied FX rate |
| `totalAmount` | Total amount charged to the sender (in source currency) |
| `receivingAmount` | Amount to be received by the beneficiary (in destination currency) |
| `fee` | Transaction fee in source currency |
| `conversionAmount` | Amount that was converted (source currency) |
| `receipt` | Regulatory disclosures that must be shown to the sender (see below) |
| `type` | Transaction type — `FX` |

#### Validation Errors

Validation failures return HTTP `400` with a per-field breakdown:

```json
{
  "errors": [
    { "fieldName": "fingerprint", "error": "A fingerprint is required (deviceData.fingerprint or additionalData.fingerprint)." },
    { "fieldName": "deviceData.userIpAddress", "error": "The device IP address is required." }
  ]
}
```

***

### Handling 3D Secure (3DS)

When the sender's card requires 3DS authentication, the create response includes:

```json
{
  "paymentStatus": "ActionRequired",
  "redirectAcsUrl": "https://..."
}
```

**Flow:**

1. Open `redirectAcsUrl` in a WebView or popup.
2. The user completes 3DS verification on the issuer's page.
3. Listen for the `postMessage` event from the payment interface to determine whether the challenge succeeded.
4. **If successful:** close the WebView and poll `GET /fx/transactions/{id}` — the transaction resumes automatically (payout integration, compliance review, etc.). `paymentStatus` returns to `null` once the challenge resolves.
5. **If failed:** close the WebView and show an error — the transaction moves to `PaymentDeclined` → `Cancelled`.

If the card is not challenged, the transaction proceeds immediately and `redirectAcsUrl` is absent. While the challenge is pending, the transaction sits in the `WaitingChallenge3ds` state.

> **Never skip the 3DS challenge.** A transaction stuck in `WaitingChallenge3ds` will not progress until the challenge completes or the transaction is cancelled.

***

### Transaction Status Lifecycle

Every transaction exposes two summary status dimensions on `GET /fx/transactions/{id}`, plus a fine-grained internal status delivered via [webhooks](webhooks.md).

#### Compliance Status

| Status | Description |
| ------ | ----------- |
| `Pending` | Screening not yet complete |
| `Approved` | Passed compliance checks — transaction can proceed |
| `Rejected` | Failed compliance review — transaction is blocked |
| `Cancelled` | Transaction was cancelled before completion |
| `Refunded` | Transaction was refunded after payment capture |
| `Failed` | Payment declined or an unrecoverable error occurred |

#### Payout Status

| Status | Description |
| ------ | ----------- |
| `Pending` | Transaction accepted, payment leg still in progress |
| `Processing` | Handed to the payout network (MSB), awaiting delivery |
| `Completed` | Funds delivered to the recipient |
| `Cancelled` | Transaction was cancelled |
| `Failed` | Payout rejected by the network, payment declined, or error |
| `Voided` | Payout voided by the payout network |
| `Refunded` | Transaction was refunded |

#### Detailed Status Flows

The fine-grained statuses below are delivered in `TransactionStatusChanged` [webhooks](webhooks.md) and in the status history endpoint.

**Card (with 3DS):**

```
Created → PaymentProcessing → WaitingChallenge3ds
  → [user completes 3DS] → PaymentAuthorized
  → ProcessingPayout → PayoutAccepted → ManualReview → ReviewApproved
  → PaymentCaptured → WaitingPayout
  → [payout network confirms delivery] → Paid → Completed
```

**Card (no 3DS):**

```
Created → PaymentProcessing → PaymentAuthorized
  → ProcessingPayout → PayoutAccepted → ManualReview → ReviewApproved
  → PaymentCaptured → WaitingPayout
  → [payout network confirms delivery] → Paid → Completed
```

**ACH:**

```
Created → PaymentProcessing → PaymentAuthorized
  → ProcessingPayout → PayoutAccepted → WaitingSettlement
  → [ACH settles] → PaymentSettled → PaymentCaptured → WaitingPayout
  → [payout network confirms delivery] → Paid → Completed
```

**Exception branches:**

* `PayoutHold` — the payout network placed a compliance hold; released automatically or by the compliance team (`PayoutHold` → `PayoutReleased` → flow resumes)
* `PayoutRejected` — the payout network rejected the transaction; it is cancelled and the payment reversed
* `ManualReview` → `ReviewRejected` — the compliance team rejected the payment; it is voided
* `BlockedPendingReview` — a risk rule or blocklist match paused the transaction for operator review before proceeding
* `PendingReversalApproval` — the payout was voided downstream and the reversal is awaiting operator approval (tenant-configurable)
* `CancelRequested` — you requested cancellation; the reversal is being processed

> Depending on your tenant configuration, `ManualReview` may be auto-approved (all transactions, card-only, or ACH-only), in which case you'll see `ManualReview` → `ReviewApproved` back-to-back.

***

### Querying Transactions

#### Get Transaction by ID

```bash
curl --request GET \
  --url https://{FQDN}/organizations/$TENANT/fx/transactions/$TRANSACTION_ID \
  --header "x-api-key: $API_KEY" \
  --header "x-agent-id: $AGENT_ID" \
  --header "x-agent-api-key: $AGENT_KEY"
```

#### List Transactions

```bash
curl --request GET \
  --url "https://{FQDN}/organizations/$TENANT/fx/transactions?page=0&size=20&senderId=$SENDER_ID" \
  --header "x-api-key: $API_KEY" \
  --header "x-agent-id: $AGENT_ID" \
  --header "x-agent-api-key: $AGENT_KEY"
```

**Query Parameters:**

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `page` | integer | Page number (0-indexed), default `0` |
| `size` | integer | Results per page, default `20` |
| `senderId` | string (UUID) | Filter by sender participant |
| `status` | string | Filter by compliance status (e.g., `APPROVED`) |

Response: `{ "total": <count>, "transactions": [ ... ] }`, most recent first.

#### Get Transaction Status History

Returns the full audit trail of status changes for a transaction, most recent first.

```bash
curl --request GET \
  --url https://{FQDN}/organizations/$TENANT/fx/transactions/$TRANSACTION_ID/status \
  --header "x-api-key: $API_KEY" \
  --header "x-agent-id: $AGENT_ID" \
  --header "x-agent-api-key: $AGENT_KEY"
```

**Sample Response:**

```json
[
  {
    "id": "2f6b3f12-a63f-43f7-9bb4-4e98dfc75fe5",
    "complianceStatus": "Approved",
    "payoutStatus": "Pending",
    "transactionId": "46b4c393-3768-4ba4-b6d2-a6398e17dd78",
    "tenantId": "master",
    "externalId": "GMT000648512199",
    "createdAt": "2024-01-03T12:54:09.455",
    "messages": [
      {
        "id": "0fbbddb8-283d-466d-b2e6-42dcbb3e67de",
        "message": "STANDARD - PAYMENT_RECEIVED",
        "createdBy": "system",
        "createdAt": "2025-11-19T13:29:52.221882",
        "type": "SystemUpdate"
      }
    ]
  }
]
```

***

### Cancelling / Reversing a Transaction

There is a **single reversal endpoint**. Inyo determines the correct reversal operation based on how far the transaction progressed:

* Payment only pre-authorized → the authorization is **voided**
* Payment already captured (but funds not yet delivered) → the payment is **refunded**

**Endpoint:** `PUT /organizations/{tenant}/fx/transactions/{transactionId}/status/cancel`\
**Authentication:** Agent-level

```bash
curl --request PUT \
  --url https://{FQDN}/organizations/$TENANT/fx/transactions/$TRANSACTION_ID/status/cancel \
  --header "x-api-key: $API_KEY" \
  --header "x-agent-id: $AGENT_ID" \
  --header "x-agent-api-key: $AGENT_KEY"
```

The reversal is processed asynchronously: the transaction moves to `CancelRequested`, then to `Cancelled` (voided) or `Refunded` once the gateway confirms.

**Responses:**

| Status | Description |
| ------ | ----------- |
| `204` | Reversal accepted and queued |
| `404` | Transaction not found |
| `422` | Transaction is not in a reversible state |

> Once the payout network has **delivered funds to the recipient** (`Paid` / `Completed`), the transaction can no longer be reversed through the API — contact Inyo support.

***

### Updating Transaction Metadata

Attach arbitrary key-value data to a transaction for your internal tracking. Returns `204`.

**Endpoint:** `PUT /organizations/{tenant}/fx/transactions/{transactionId}/metadata`\
**Authentication:** Agent-level

```bash
curl --request PUT \
  --url https://{FQDN}/organizations/$TENANT/fx/transactions/$TRANSACTION_ID/metadata \
  --header 'Content-Type: application/json' \
  --header "x-api-key: $API_KEY" \
  --header "x-agent-id: $AGENT_ID" \
  --header "x-agent-api-key: $AGENT_KEY" \
  --data '{
  "internalRef": "INV-2025-001",
  "department": "treasury"
}'
```

***

### Batch Transactions

For high-volume use cases you can submit multiple transactions in one call. Each item uses the same shape as the single-create request.

| Operation | Method | Endpoint |
| --------- | ------ | -------- |
| Submit batch | `POST` | `/organizations/{tenant}/fx/transactions/batch` |
| Retry a failed batch item | `POST` | `/organizations/{tenant}/fx/transactions/batch/{transactionId}/retry` |

```json
{ "transactions": [ { "senderId": "...", "recipientId": "...", "...": "..." } ] }
```

Both endpoints return `202 Accepted`; individual transactions progress independently — track them via webhooks or polling.

***

### Transaction Limits

Before executing a transaction, verify the sender has not exceeded their limits. The numbers reported here are computed with the same rules the transaction validator applies, so what shows as `available` is what a transaction can actually use.

**Endpoint:** `GET /organizations/{tenant}/fx/participants/{participantId}/limits`\
**Authentication:** Agent-level

```bash
curl --request GET \
  --url https://{FQDN}/organizations/$TENANT/fx/participants/$SENDER_ID/limits \
  --header "x-api-key: $API_KEY" \
  --header "x-agent-id: $AGENT_ID" \
  --header "x-agent-api-key: $AGENT_KEY"
```

**Sample Response:**

```json
{
  "oneDayLimit": {
    "limit": { "amount": "2999.00", "currency": "USD" },
    "used": { "amount": "0.00", "currency": "USD" },
    "available": { "amount": "2999.00", "currency": "USD" }
  },
  "thirtyDaysLimit": {
    "limit": { "amount": "6000.00", "currency": "USD" },
    "used": { "amount": "2959.99", "currency": "USD" },
    "available": { "amount": "3040.01", "currency": "USD" }
  },
  "oneHundredAndEightyDaysLimit": {
    "limit": { "amount": "9999.00", "currency": "USD" },
    "used": { "amount": "5000.00", "currency": "USD" },
    "available": { "amount": "4999.00", "currency": "USD" }
  }
}
```

To see which fields are needed to reach the next compliance level (and unlock higher limits), use `GET /participants/{id}/complianceLevels` — see [Trust Level Limits](sender/trust-level-limits.md).

***

### Receipts (Regulatory Requirement)

As an agent of a licensed Money Service Business, you are **legally required** to issue a receipt to the sender immediately after transaction submission. The transaction response includes a `receipt` object with mandatory regulatory disclosures that **must be displayed verbatim** to the end user.

The receipt must include:

1. **Exchange rate** — the locked-in rate from the quote
2. **Fees** — total fees charged to the customer
3. **Total amount** — full amount paid by the sender
4. **Receive amount** — exact amount to be received by the beneficiary
5. **Regulatory disclosures** — dynamic legal text specific to the corridor (e.g., right to refund, cancellation policy)

> You **must not** alter the financial data or regulatory text returned by the API. Display it as-is.

***

### Sandbox: Forcing Status Transitions

In sandbox/staging you can simulate external events (gateway callbacks, payout notifications) to drive a transaction through its lifecycle without real money movement. See [Sandbox Testing](sandbox-testing.md).

***

### Error Responses

| HTTP Status | Cause |
| ----------- | ----- |
| `400` | Missing required fields, invalid data format, missing fingerprint/IP, or expired quote |
| `403` | Agent not approved, or sender doesn't meet compliance requirements |
| `404` | Referenced transaction/participant/account not found in your tenant |
| `422` | Business rule violation (e.g., limit exceeded, invalid corridor, non-reversible state) |

***

### All Endpoints

| Operation | Method | Endpoint |
| --------- | ------ | -------- |
| Create transaction | `POST` | `/organizations/{tenant}/fx/transactions` |
| Get transaction | `GET` | `/organizations/{tenant}/fx/transactions/{transactionId}` |
| List transactions | `GET` | `/organizations/{tenant}/fx/transactions` |
| Get status history | `GET` | `/organizations/{tenant}/fx/transactions/{transactionId}/status` |
| Cancel / reverse transaction | `PUT` | `/organizations/{tenant}/fx/transactions/{transactionId}/status/cancel` |
| Update metadata | `PUT` | `/organizations/{tenant}/fx/transactions/{transactionId}/metadata` |
| Submit batch | `POST` | `/organizations/{tenant}/fx/transactions/batch` |
| Retry batch item | `POST` | `/organizations/{tenant}/fx/transactions/batch/{transactionId}/retry` |
| Check limits | `GET` | `/organizations/{tenant}/fx/participants/{participantId}/limits` |
| Get required/optional fields | `GET` | `/organizations/{tenant}/fx/transactions/schema?countryCode={iso2}` |
| Force status change (sandbox) | `POST` | `/organizations/{tenant}/fx/transactions/{transactionId}/events` |
