Skip to content
Last updated

Internal Transactions move funds book-to-book between two Monato accounts (same institution). These do not go through SPEI, so there’s no CEP and settlement is typically near-real-time when both instruments are active and funded.


Overview

  • Rail: Internal (book-to-book)

  • CEP: Not applicable

  • Latency: Near-real-time

  • Use cases:

    • Move funds between Cost Centers / Business Units
    • Intra-merchant payouts
    • Reserve account management
    • Move funds to another Monato customer
  • Webhooks / Notifications:

    • The credit leg of a successful internal transaction may generate a MONEY_IN webhook to the destination instrument (the client that owns the destination instrument), only if they have a MONEY_IN webhook registered.
    • Webhook emission depends on ownership (see Webhooks / Notifications section below).
    • Payload format matches the Money In confirmation guide, with sub_category = "INT_CREDIT".

Parity with Money Out: Request body format and most validations are the same as Money Out, with a few extra internal checks noted below.


Endpoint

POST /v1/transactions/internal_transaction


Request

Path params: none
Query params: none

Headers

  • Authorization: Bearer <JWT>
  • Content-Type: application/json

Body

{
  "client_id": "c2d1d1e3-3340-4170-980e-e9269bbbc551",
  "source_instrument_id": "709448c3-7cbf-454d-a87e-feb23801269a",
  "destination_instrument_id": "dd7f8d89-94dd-43ca-871b-720fde378b52",
  "transaction_request": {
    "amount": "1.90",
    "currency": "MXN",
    "description": "Internal transfer",
    "external_reference": "1238766"
  }
}

Field notes

  • source_instrument_id, destination_instrument_id

    IDs of two valid internal instruments. They can belong either to your client or to one of your customers, as long as both instruments are internal to Monato.

To discover internal instruments available for your client or customers, use:

GET /v1/clients/{clientId}/instruments

with the optional customer_id filter.

Validations

Field-level

  • amount
    • Must be a numeric string with 2 decimal places (e.g., "1.90").
    • Must be greater than 0 (otherwise backend returns DATA_ERROR: "Transaction Amount must be higher than 0.").
  • currency
    • Only "MXN" is supported (otherwise: DATA_ERROR: "Transaction currency unsupported.").
  • description
    • Length < 40 characters (if exceeded: DATA_ERROR: "Transaction description must have less than 40 characters length.").
  • external_reference
    • Must be numeric and max 7 digits (otherwise: DATA_ERROR: "External reference should be numeric and have a maximum length of 7 digits.").
  • IDs
    • client_id, source_instrument_id, destination_instrument_id must be valid UUIDs.

Instrument state

  • Source
    • Exists, is active, not blocked, and has sufficient funds
      (if not: FAILED_PRECONDITION: "The account does not have sufficient funds.").
  • Destination
    • Exists / is assigned, active, and not blocked
      (if not: FAILED_PRECONDITION: "The account is not currently active.").
  • Same institution (internal rail)
    • Destination must be internal to Monato (if not: 409 external_transfer_not_allowed).
  • Different instruments
    • Best practice: source_instrument_id destination_instrument_id.

Response examples

Success (LIQUIDATED)

{
    "id": "09c9caac-3b74-4690-8ac5-5a01b2559b3f",
    "bankId": "d3435bd9-998d-4e8a-9067-6b71d5fd3ac7",
    "clientId": "b000654b-4d12-46e5-b451-662459b6effc",
    "externalReference": "1238801",
    "trackingId": "20250925FINCHCUCHFGMRLZ",
    "description": "Prueba Internal 25/09/25",
    "amount": "1.00",
    "currency": "MXN",
    "category": "INTER_TRANS",
    "subCategory": "INT_DEBIT",
    "transactionStatus": "LIQUIDATED",
    "audit": {
        "createdAt": "2025-09-25 15:48:28.486316-06:00",
        "updatedAt": "2025-09-25 15:48:28.773897-06:00",
        "deletedAt": "None",
        "blockedAt": "None"
    }
}

Error Examples

Inactive account — 400 FAILED_PRECONDITION (10-E4120)

{
    "code": 9,
    "message": "API Error",
    "details": [
        {
            "@type": "type.googleapis.com/google.rpc.ErrorInfo",
            "reason": "FAILED_PRECONDITION",
            "domain": "CORE",
            "metadata": {
                "error_detail": "The account is not currently active.",
                "http_code": "400",
                "module": "Transactions",
                "method_name": "InternalTransaction",
                "error_code": "10-E4120"
            }
        }
    ]
}

Insufficient funds — 400 FAILED_PRECONDITION (10-E4120)

{
    "code": 9,
    "message": "API Error",
    "details": [
        {
            "@type": "type.googleapis.com/google.rpc.ErrorInfo",
            "reason": "FAILED_PRECONDITION",
            "domain": "CORE",
            "metadata": {
                "error_detail": "The account does not have sufficient funds.",
                "http_code": "400",
                "module": "Transactions",
                "method_name": "InternalTransaction",
                "error_code": "10-E4120"
            }
        }
    ]
}

Invalid External reference — 400 DATA_ERROR (10-E4120)

{
    "code": 9,
    "message": "API Error",
    "details": [
        {
            "@type": "type.googleapis.com/google.rpc.ErrorInfo",
            "reason": "DATA_ERROR",
            "domain": "CORE",
            "metadata": {
                "error_detail": "External reference should be numeric and have a maximum length of 7 digits.",
                "http_code": "400",
                "module": "Transactions",
                "method_name": "InternalTransaction",
                "error_code": "10-E4120"
            }
        }
    ]
}

Invalid amount (<= 0) — 400 DATA_ERROR (10-E4120)

{
    "code": 9,
    "message": "API Error",
    "details": [
        {
            "@type": "type.googleapis.com/google.rpc.ErrorInfo",
            "reason": "DATA_ERROR",
            "domain": "CORE",
            "metadata": {
                "error_detail": "Transaction Amount must be higher than 0.",
                "http_code": "400",
                "module": "Transactions",
                "method_name": "InternalTransaction",
                "error_code": "10-E4120"
            }
        }
    ]
}

Large description — 400 DATA_ERROR (10-E4120)

{
    "code": 9,
    "message": "API Error",
    "details": [
        {
            "@type": "type.googleapis.com/google.rpc.ErrorInfo",
            "reason": "DATA_ERROR",
            "domain": "CORE",
            "metadata": {
                "error_detail": "Transaction description must have less than 40 characters length.",
                "http_code": "400",
                "module": "Transactions",
                "method_name": "InternalTransaction",
                "error_code": "10-E4120"
            }
        }
    ]
}

Currency not supported — 400 DATA_ERROR (10-E4120)

{
    "code": 9,
    "message": "API Error",
    "details": [
        {
            "@type": "type.googleapis.com/google.rpc.ErrorInfo",
            "reason": "DATA_ERROR",
            "domain": "CORE",
            "metadata": {
                "error_detail": "Transaction currency unsupported.",
                "http_code": "400",
                "module": "Transactions",
                "method_name": "InternalTransaction",
                "error_code": "10-E4120"
            }
        }
    ]
}

Webhooks / Notifications

A successful internal transaction returns the debit leg (source side) in the POST /v1/transactions/internal_transaction response.

Additionally, the credit leg may trigger a MONEY_IN webhook, depending on ownership:

  • Different owner (inbound credit): If the destination instrument belongs to a different owner_id than the initiator (even if the client_id is the same), Monato sends a MONEY_IN webhook to the client that owns the destination instrument (only if a MONEY_IN webhook is registered).

    • sub_category = "INT_CREDIT"
  • Same owner (self-transfer): If the destination instrument belongs to the same client_id and same owner_id as the initiator, no MONEY_IN webhook is emitted.

    • No webhook event is created for replay/resend.

    • In this case, the API response is the source of truth (and you can also fetch the transaction by id for reconciliation).

Resend / replay behavior (Dashboard)

The dashboard “resend webhook” action only replays already generated webhook events.

  • For same-owner self-transfers, since no webhook event is created, there is nothing to resend.
  • Showing “resend webhook” for same-owner internal transfers is a known UI bug and we are addressing it.

MONEY_IN webhook payload (when emitted)

  • Uses the common MONEY_IN envelope:

    {
      "id_msg": "6daea2d2-ccb0-48f3-917c-f387dc8e99b0",
      "msg_name": "MONEY_IN",
      "msg_date": "2025-11-20",
      "body": {
        "...": "..."
      }
    }
  • Includes, in the body, fields like:

    {
      "id": "a0037594-5064-4dda-896b-f9b5dd4988dd",
      "beneficiary_account": "734185000000001177",
      "beneficiary_name": "MERCHANT TEST",
      "beneficiary_rfc": "FTR230125Q00",
      "payer_account": "734185000000000822",
      "payer_name": "Customer Test-1 Legal",
      "payer_rfc": "ND",
      "payer_institution": "90734",
      "amount": "1.00",
      "transaction_date": "2025-11-20 15:05:59",
      "tracking_key": "20251120FINCHESDHI7FVTU",
      "payment_concept": "CUST - SPEI",
      "numeric_reference": "1100003",
      "sub_category": "INT_CREDIT",
      "registered_at": "2025-11-20T15:05:59.915184-06:00",
      "owner_id": "24f1e5d5-4045-4b1a-a0c4-5e6c6b1d44ef"
    }

See the Money In confirmation documentation for the full field-by-field description of the MONEY_IN webhook payload.

For internal credits, the webhook is informational:

  • The funds are already moved book-to-book when the webhook is sent.
  • HTTP status codes on your response do not trigger an automatic rollback or refund.

Endpoint error mapping (quick reference)

Failing ruleTypical response
Source account inactive/blocked400 FAILED_PRECONDITION (error_code: 10-E4120)
Insufficient funds (source)400 FAILED_PRECONDITION (error_code: 10-E4120)
external_reference non-numeric or > 7 digits400 DATA_ERROR (error_code: 10-E4120)
amount ≤ 0400 DATA_ERROR (error_code: 10-E4120)
description ≥ 40 characters400 DATA_ERROR (error_code: 10-E4120)
currency other than MXN400 DATA_ERROR (error_code: 10-E4120)
Destination not found / not assigned404 destination_not_found
Destination is external (not internal rail)409 external_transfer_not_allowed

Categories and subCategories for internal transactions

  • Debit leg (source account):

    • category = "INTER_TRANS"
    • subCategory = "INT_DEBIT"
  • Credit leg (destination account, as seen in the MONEY_IN webhook or related transaction):

    • category = "INTER_TRANS"
    • subCategory = "INT_CREDIT"

⚠️ Disclaimer
We plan to unify the APIs in the future so that both external Money Out (SPEI) and internal transactions (between Monato accounts) are supported through a single endpoint.

For now, please use the endpoints separately as documented:

  • Use POST /v1/transactions/internal_transaction for internal (book-to-book) movements.
  • Use the Money Out endpoints for external SPEI transfers.
  • There is no /refund endpoint for internal transactions. If you need to “reverse” an internal movement, you must create a new internal_transaction in the opposite direction.