Skip to content
Last updated

Internal Transactions API

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 every successful internal transaction generates a MONEY_IN webhook towards the client that owns the destination instrument (if they have a MONEY_IN webhook registered).
    • The 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"
            }
        }
    ]
}

Money In webhook for internal transactions

When an internal transaction is successfully processed:

  • The POST /v1/transactions/internal_transaction response returns the debit leg (source side).
  • Additionally, the client that owns the destination instrument receives a MONEY_IN webhook.

The webhook:

  • 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.

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.