Implementing Peer-to-Peer Transfers using Unified Transfer

⚠️

Unified Transfer is in active development. As a pioneer merchant, you will be informed of the updates through your Maya Relationship Manager.

Overview

This guide walks you through the step-by-step process of integrating Unified Transfer to enable peer-to-peer (P2P) fund transfers across financial institutions in the Philippines.

By the end of this guide, you will be able to authenticate, initiate, confirm, and monitor fund transfers in the Sandbox and Production environments.

Prerequisites

Before starting integration, ensure the following requirements are completed:


Integrating with Unified Transfer

Unified Transfer consists of four runtime stages:

  1. Authenticate
  2. Initiate Transfer
  3. Confirm Transfer
  4. Monitor Status

Every protected API call requires:

  • Authorization: Bearer {access_token}
  • x-jws-signature: {detached_jws_signature}

Step 1: Authenticate (Obtain Access Token)

Before calling any Unified Transfer endpoint, obtain a Bearer token using the OAuth 2.0 Client Credentials Flow. Refer to Bearer Authentication

You can reuse the access token until it expires.

Step 2: Initiate Fund Transfer

Step 2.1: Build the Request Body

Create a transfer initiation object containing debit account, credit account, amount details, and optional KYC information.

Required Parameters:

  • debit_account - Source account information (financial_institution_code, account_number)
  • credit_account - Destination account information (financial_institution_code, account_number, account_name)
  • amount - Transfer amount and currency (PHP only)

Optional Parameters:

  • ach_channel - ACH channel (instapay or pesonet, defaults to instapay for non-Maya institutions)
  • sender - Sender KYC information (name, nationality, birth_date, present_address, income_source, identification)
  • receiver - Receiver KYC information
  • transaction_purpose - Purpose of the transaction
  • origin_country - Country where transaction originated

Example Request Object (Minimum):

{
  "data": {
    "initiation": {
      "debit_account": {
        "financial_institution_code": "PAPHPHM1XXX",
        "account_number": "041279562523"
      },
      "credit_account": {
        "financial_institution_code": "MBTCPHMMXXX",
        "account_number": "772356410242",
        "account_name": "Maria Reyes"
      },
      "amount": {
        "currency": "PHP",
        "value": 1000.00
      },
      "ach_channel": "instapay",
      "transaction_purpose": "Family Support/Allowance"
    }
  }
}

Example Request Object (With Full KYC):

{
  "data": {
    "initiation": {
      "debit_account": {
        "financial_institution_code": "PAPHPHM1XXX",
        "account_number": "041279562523"
      },
      "credit_account": {
        "financial_institution_code": "MBTCPHMMXXX",
        "account_number": "772356410242",
        "account_name": "Maria Reyes"
      },
      "amount": {
        "currency": "PHP",
        "value": 5000.00
      },
      "ach_channel": "pesonet",
      "transaction_purpose": "Business Capital",
      "sender": {
        "name": {
          "first_name": "Juan",
          "middle_name": "Dela",
          "last_name": "Cruz"
        },
        "contact_information": {
          "type": "msisdn",
          "value": "+639498187453"
        },
        "nationality": "PH",
        "gender": "M",
        "birth_date": "1985-01-01",
        "present_address": {
          "line1": "Blk 1 Lot 24 Maginhawa St.",
          "locality": "Bgry. Nagkaisang Nayon",
          "state": "Metro Manila",
          "zip_code": "1240",
          "city": "Quezon City",
          "country": "PH"
        },
        "income_source": "Business Proceeds",
        "identification": {
          "type": "Passport",
          "number": "110-230-987-000",
          "issue_country": "PH",
          "issue_date": "2020-01-01",
          "expiry_date": "2030-01-01"
        },
        "relationship_to_receiver": "Business Partner"
      }
    }
  }
}

Note: For accepted values (KYC types, income sources, transaction purposes, etc.), refer to Financial Institution Codes and Standard Field Values in Unified Transfer

Step 2.2: Sign the Request Body

Sign the exact JSON body using JWS (JSON Web Signature) with your private key to ensure request integrity and authenticity. See JWS Implementation Guide for Unified Transfer

Important Reminder:

  • Sign the exact body sent to the API.
  • Do not modify formatting after signing.
  • Generate a new signature per request.

Step 2.3: Send the Initiate Request

Send the signed initiate request to the Initiate Transfer endpoint with the required headers.

POST /v1/transfers/p2p
Authorization: Bearer {access_token}
x-jws-signature: {jws_signature}
x-idempotency-key: {uuid}
x-originator-transaction-id: {unique_transaction_id}
x-fapi-interaction-id: {uuid}
Content-Type: application/json

Required Headers:

  • Authorization - Bearer token from OAuth 2.0 authentication (see Step 1)
  • x-jws-signature - JWS signature of the request body (from Step 2.2)
  • x-idempotency-key - UUID to prevent duplicate requests
  • x-originator-transaction-id - Your unique transaction identifier for tracking
  • x-fapi-interaction-id - UUID for request tracing (optional but recommended)
  • Content-Type - Must be application/json

Success Response (201 Created):

A successful Initiate Transfer creates the transfer intent with the initial status INITIATED.

{
  "data": {
    "id": "c36d9958-9c55-49e3-b70e-702b082046c0",
    "status": "INITIATED",
    "created_timestamp": "2025-01-08 09:22:12.212",
    "confirmation_deadline": "2025-01-08 10:22:12.212",
    "initiation": {
      "debit_account": {
        "financial_institution_code": "PAPHPHM1XXX",
        "account_number": "041279562523"
      },
      "credit_account": {
        "financial_institution_code": "MBTCPHMMXXX",
        "account_number": "772356410242",
        "account_name": "Maria Reyes"
      },
      "amount": {
        "currency": "PHP",
        "value": 1000.00
      }
    },
    "transfer_details": {
      "gross_amount": {
        "currency": "PHP",
        "value": 1007.00
      },
      "principal_amount": {
        "currency": "PHP",
        "value": 1000.00
      },
      "fee": {
        "currency": "PHP",
        "value": 7.00
      }
    }
  }
}

Error Response (400 Bad Request):

{
  "errors": [
    {
      "code": "TRGINIT001",
      "description": "One or more fields have invalid values (headers or body). Please review the list of invalid fields provided in the response.",
      "parameters": [
        {
          "field": "credit_account.account_name",
          "desc": "Name must be up to 140 characters and must not contain invalid characters."
        }
      ]
    }
  ]
}

Step 3: Confirm Transfer

After successful initiation, confirm the transfer to begin processing.

If not confirmed within 1 hour, the status becomes LAPSED.

Step 3.1: Sign the Empty Payload

Confirm Transfer has no request body, but you still need to sign the empty body with your private key to ensure request integrity and authenticity. For signing the request, refer to JWS Implementation Guide for Unified Transfer.

Step 3.2: Send the Confirmation Request

Send the signed confirmation request to the Confirm Transfer endpoint.

PUT /v1/transfers/p2p/{id}/confirmation
Authorization: Bearer {access_token}
x-jws-signature: {jws_signature}
x-fapi-interaction-id: {uuid}

Required Headers:

  • Authorization - Bearer token from OAuth 2.0 authentication (see Step 1)
  • x-jws-signature - JWS signature of the request body (from Step 3.1)
  • x-fapi-interaction-id - UUID for request tracing (optional but recommended)

Example Request:

PUT /v1/transfers/p2p/c36d9958-9c55-49e3-b70e-702b082046c0/confirmation
Authorization: Bearer eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQ...
x-jws-signature: eyJhbGciOiJSUzI1NiIsImtpZCI6InlvdXIta2V5LWlkIn0...

Success Response (202 Accepted):

A successful confirmation, the status of the transfer intent will transition to PROCESSING status.

{
  "data": {
    "id": "c36d9958-9c55-49e3-b70e-702b082046c0",
    "status": "PROCESSING",
    "created_timestamp": "2025-01-08 09:22:12.212",
    "updated_timestamp": "2025-01-08 09:22:46.898",
    "confirmation_deadline": "2025-01-08 10:22:12.212",
    "initiation": { /* same as initiate response */ },
    "transfer_details": { /* same as initiate response */ }
  }
}

Error Response (404 Not Found):

{
  "errors": [
    {
      "code": "TRGCONF002",
      "description": "The fund transfer you are confirming does not exist. Please initiate a fund transfer first."
    }
  ]
}

Step 4: Monitor Transfer Status

You can track the transfer status using the transaction ID or originator transaction ID, or wait for the final status notification be delivered to your callback endpoint.

4.1. Polling to Status Inquiry Endpoints

Step 4.1.1: Sign the Empty Body

Status Inquiry has no request body, but you still need to sign the empty body with your private key to ensure request integrity and authenticity.

For signing the request, refer to JWS Implementation Guide for Unified Transfer.

Step 4.1.2: Send the Request to the API

Status Inquiry by ID:

GET /v1/transfers/p2p/{id}
Authorization: Bearer {access_token}
x-jws-signature: {jws_signature}

Status Inquiry by Originator Transaction ID:

GET /v1/transfers/p2p?x-originator-transaction-id={transaction_id}
Authorization: Bearer {access_token}
x-jws-signature: {jws_signature}

4.2. Receive Final Status via Callback

Unified Transfer notifies your system of the transaction status updates via your Callback endpoint. To get started with Callback, see Implementing Callback for Unified Transfer.


Key Integration Rules

  • Always sign every protected request.
  • Always send idempotency keys for initiation.
  • Never reuse JWS signatures.
  • Reuse access tokens until expiry.
  • Confirm transfers within 1 hour.

Sample Complete Integration Flow (Python)

import requests
import jwt
import json
from datetime import datetime

class UnifiedTransferClient:
    def __init__(self, base_url, private_key_path, key_id):
        self.base_url = base_url
        self.private_key = open(private_key_path, 'r').read()
        self.key_id = key_id
        self.access_token = None
  
    def get_access_token(self, client_id, client_secret):
        """Get OAuth2 access token"""
        auth_url = f"{self.base_url}/token"
        response = requests.post(auth_url, data={
            'grant_type': 'client_credentials',
            'scope': 'transfer'
        }, auth=(client_id, client_secret))
    
        self.access_token = response.json()['access_token']
        return self.access_token
  
    def create_jws_signature(self, payload):
        """Create JWS signature for request body"""
        return jwt.encode(
            payload,
            self.private_key,
            algorithm='RS256',
            headers={'kid': self.key_id}
        )
  
    def initiate_transfer(self, transfer_data, idempotency_key, originator_txn_id):
        """Initiate a P2P transfer"""
        url = f"{self.base_url}/v1/transfers/p2p"
        jws_signature = self.create_jws_signature(transfer_data)
    
        headers = {
            'Authorization': f'Bearer {self.access_token}',
            'x-jws-signature': jws_signature,
            'x-idempotency-key': idempotency_key,
            'x-originator-transaction-id': originator_txn_id,
            'Content-Type': 'application/json'
        }
    
        response = requests.post(url, json=transfer_data, headers=headers)
        return response.json()
  
    def confirm_transfer(self, transfer_id):
        """Confirm a transfer for processing"""
        url = f"{self.base_url}/v1/transfers/p2p/{transfer_id}/confirmation"
    
        headers = {
            'Authorization': f'Bearer {self.access_token}',
            'x-jws-signature': self.create_jws_signature({}),
            'Content-Type': 'application/json'
        }
    
        response = requests.put(url, headers=headers)
        return response.json()

# Usage example
client = UnifiedTransferClient(
    base_url='https://sandbox.api.maya.ph',
    private_key_path='private_key.pem',
    key_id='your-key-id'
)

# Get access token
client.get_access_token('your_client_id', 'your_client_secret')

# Initiate transfer
transfer_data = {
    "data": {
        "initiation": {
            "debit_account": {
                "financial_institution_code": "PAPHPHM1XXX",
                "account_number": "041279562523"
            },
            "credit_account": {
                "financial_institution_code": "MBTCPHMMXXX",
                "account_number": "772356410242",
                "account_name": "Maria Reyes"
            },
            "amount": {
                "currency": "PHP",
                "value": 1000.00
            }
        }
    }
}

result = client.initiate_transfer(
    transfer_data,
    'a6b59599-a1e3-47cd-b9d7-ffaf56a2f1fe',
    'TXN20250108001'
)

if result['data']['status'] == 'INITIATED':
    # Confirm the transfer
    confirm_result = client.confirm_transfer(result['data']['id'])
    print(f"Transfer confirmed: {confirm_result['data']['status']}")

Endpoints

By now, you should understand the essentials of integrating Unified Transfer, including:

  • Which endpoints to call during the payment flow
  • When to use each endpoint

Below is the summary of the core endpoints for Unified Transfer integration.

Partner-hosted Endpoints

NameMethodDescription
JWKS EndpointA JSON Web Key Set (JWKS) endpoint that hosts your public signing keys, which Maya uses to verify the digital signatures on your API requests.
Callback EndpointPOSTYour callback endpoint, where you receive the transaction updates from Maya. It should be able to receive HTTP POST requests.

Unified Transfer Endpoints

Every Unified Transfer endpoint requires both an OAuth 2.0 Bearer token and a JWS signature of the request body. See How API Authentication Works in Unified Transfer.

NameMethodEndpointDescription
Initiate TransferPOST/v1/transfers/p2pUse this API to initiate a secure fund transfer between accounts. All fields will be validated before processing. This API returns the id which you will use to confirm the processing of the fund transfer transaction.
Confirm TransferPUT/v1/transfers/p2p/{id}/confirmationUse this API to confirm the processing of an initiated fund transfer between accounts. The id returned in the Initiate Transfer will be needed in the Confirm Transfer API call.
Inquire Status using x-originator-transaction-idGET/v1/transfers/p2pRetrieve detailed information about a previously initiated fund transfer using x-originator-transaction-id query parameter.
Inquire Status using IDGET/v1/transfers/p2p/{id}Retrieve detailed information about a previously initiated fund transfer using its unique transaction ID.

API Sequence

The sequence diagram above illustrates the complete P2P transfer flow from authentication to completion, showing the two-step process (initiate and confirm) and asynchronous callback notifications for final status updates.

Asynchronous Processing

Unified Transfer uses asynchronous processing for fund transfers:

Asynchronous Processing:

  • Transfer initiation returns INITIATED status after validation
  • Transfer confirmation returns PROCESSING status
  • Final status (APPROVED or DECLINED) is delivered via callback notifications
  • Callback notifications include retry mechanism with up to 5 callback attempts (with exponential backoff between attempts)
  • Status polling available using inquiry endpoints for systems without callback capability

Transfer Status Flow:

  1. INITIATED - Transfer validated and ready for confirmation
  2. PROCESSING - Transfer confirmed and being processed asynchronously
  3. APPROVED - Transfer completed successfully (via callback)
  4. DECLINED - Transfer failed or rejected (via callback)
  5. LAPSED - Transfer not confirmed within 1-hour deadline

FAQs

Q: What is the maximum transfer amount?

A: Transfer limits depend on the processing method used:

  • InstaPay: PHP 50,000.00 per transaction (real-time processing for external institutions)
  • PESONet: PHP 300,000.00 per transaction (batch processing for external institutions)
  • Maya: Account-specific limits (see Maya Account Limits for details)

Q: How long is a transfer valid for confirmation?

A: Initiated transfers must be confirmed within 1 hour, otherwise they will expire with status LAPSED.

Q: What ACH channels are supported?

A: The API supports InstaPay (real-time processing) and PESONet (batch processing) channels for external institutions. InstaPay is the default channel for transfers to non-Maya institutions and provides faster processing, while PESONet supports higher transaction amounts. Maya institutions use internal processing for potentially faster settlement.

Q: Can I cancel a transfer after confirmation?

A: No, transfers cannot be cancelled once confirmed and moved to PROCESSING status. Only INITIATED transfers can expire if not confirmed within the deadline.

Q: What information is required for a transfer?

A: Only three fields are mandatory: debit_account (source account), credit_account (destination account with account name), and amount. Additional fields like sender KYC information, transaction purpose, and ACH channel are optional.

Q: How do I get the final transfer status?

A: Final status (APPROVED or DECLINED) is delivered via callback notifications. You can also poll the status using the inquiry endpoints if callbacks are not available.

Q: What financial institution codes are supported?

A: Use SWIFT/BIC codes (11 characters) for financial institutions. The API supports transfers across over 100 Philippine financial institutions through InstaPay, PESONet, and Maya Group in-house processing. For the complete list of supported institutions and their codes, see Financial Institution Codes and Standard Field Values in Unified Transfer.


Next Steps

You have successfully implemented peer-to-peer (P2P) fund transfer using Unified Transfer. To build a more robust and production-ready integration, we recommend exploring the following: