Skip to content
Last updated

This guide will walk you through integrating Monato's Cash API into your application. You'll learn how to configure webhooks and perform operations using your provided API credentials.

Prerequisites

  • Contact our customer success team to obtain your API credentials (api_key and api_secret)
  • A webhook endpoint to receive status notifications
  • Basic understanding of REST APIs and HMAC authentication

Note: Clients are created by our customer success team. Once your client account is set up, you'll receive your unique api_key and api_secret for API authentication.

Step 1: Configure Webhooks

Set up webhook endpoints to receive real-time notifications about operations status changes.

Generate HMAC Signature

Before making the request, you need to generate an HMAC-SHA256 signature. Select your programming language:

JavaScript/Node.js
const crypto = require('crypto');

const timestamp = Math.floor(Date.now() / 1000).toString();
const requestBody = JSON.stringify({
  endpoint_url: "https://your-webhook-endpoint.com/webhooks"
});
const payload = `${timestamp}.${requestBody}`;
const apiSecret = "your_64_character_api_secret_here";

const signature = crypto
  .createHmac('sha256', apiSecret)
  .update(payload)
  .digest('hex');
Python
import hmac
import hashlib
import json
import time

timestamp = str(int(time.time()))
request_body = json.dumps({
    "endpoint_url": "https://your-webhook-endpoint.com/webhooks"
})
payload = f"{timestamp}.{request_body}"
api_secret = "your_64_character_api_secret_here"

signature = hmac.new(
    api_secret.encode('utf-8'),
    payload.encode('utf-8'),
    hashlib.sha256
).hexdigest()
PHP
<?php
$timestamp = time();
$requestBody = json_encode([
    'endpoint_url' => 'https://your-webhook-endpoint.com/webhooks'
]);
$payload = $timestamp . '.' . $requestBody;
$apiSecret = 'your_64_character_api_secret_here';

$signature = hash_hmac('sha256', $payload, $apiSecret);
?>
Ruby
require 'openssl'
require 'json'
require 'time'

timestamp = Time.now.to_i.to_s
request_body = {
  endpoint_url: "https://your-webhook-endpoint.com/webhooks"
}.to_json
payload = "#{timestamp}.#{request_body}"
api_secret = "your_64_character_api_secret_here"

signature = OpenSSL::HMAC.hexdigest('SHA256', api_secret, payload)
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import com.fasterxml.jackson.databind.ObjectMapper;

long timestamp = Instant.now().getEpochSecond();
String requestBody = new ObjectMapper().writeValueAsString(
    Map.of("endpoint_url", "https://your-webhook-endpoint.com/webhooks")
);
String payload = timestamp + "." + requestBody;
String apiSecret = "your_64_character_api_secret_here";

Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(apiSecret.getBytes(), "HmacSHA256");
mac.init(secretKeySpec);
String signature = bytesToHex(mac.doFinal(payload.getBytes()));

private static String bytesToHex(byte[] bytes) {
    StringBuilder result = new StringBuilder();
    for (byte b : bytes) {
        result.append(String.format("%02x", b));
    }
    return result.toString();
}
C#
using System;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;

long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string requestBody = JsonConvert.SerializeObject(new {
    endpoint_url = "https://your-webhook-endpoint.com/webhooks"
});
string payload = $"{timestamp}.{requestBody}";
string apiSecret = "your_64_character_api_secret_here";

using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiSecret)))
{
    byte[] hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
    string signature = BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
}
Go
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "time"
)

type RequestBody struct {
    EndpointURL string `json:"endpoint_url"`
}

func main() {
    timestamp := fmt.Sprintf("%d", time.Now().Unix())
    requestBody := RequestBody{
        EndpointURL: "https://your-webhook-endpoint.com/webhooks",
    }
    requestBodyJSON, _ := json.Marshal(requestBody)
    payload := fmt.Sprintf("%s.%s", timestamp, string(requestBodyJSON))
    apiSecret := "your_64_character_api_secret_here"

    h := hmac.New(sha256.New, []byte(apiSecret))
    h.Write([]byte(payload))
    signature := hex.EncodeToString(h.Sum(nil))
}

Configure Webhook

curl -X POST "https://your-domain.com/api/v1/cash/webhooks" \
  -H "Content-Type: application/json" \
  -H "X-Client-Id: a1b2c3d4e5f6g7h8" \
  -H "X-Signature: abc123def456..." \
  -H "X-Timestamp: 1705312200" \
  -d '{
    "endpoint_url": "https://your-webhook-endpoint.com/webhooks"
  }'

Response

{
  "event": "webhook.created",
  "id": 456,
  "endpoint_url": "https://your-webhook-endpoint.com/webhooks",
  "secret_token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6",
  "created_at": "2025-01-15T10:30:00Z"
}

Note: The webhook will be automatically tested and only activated if the test succeeds.

Webhook Activation Test

After configuring your webhook, our system will automatically send a test request to verify your endpoint is working correctly. Your endpoint should respond with HTTP 200-299 for the webhook to be activated.

Test Request Your Endpoint Will Receive

POST https://your-webhook-endpoint.com/webhooks
Content-Type: application/json
X-Webhook-Timestamp: 1705312200
X-Webhook-Signature: abc123def456...

{
  "event": "webhook.activation",
  "processed_at": "2025-01-15T10:30:00Z"
}

Verify the Activation Signature

The activation request includes a signature that you should verify using your webhook's secret_token. Select your programming language:

JavaScript/Node.js
const crypto = require('crypto');

function verifyActivationSignature(payload, signature, secretToken) {
  const expectedSignature = crypto
    .createHmac('sha256', secretToken)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );
}

// Example usage
const payload = JSON.stringify({
  "event": "webhook.activation",
  "processed_at": "2025-01-15T10:30:00Z"
});
const isValid = verifyActivationSignature(
  payload,
  "abc123def456...",
  "your_webhook_secret_token"
);
Python
import hmac
import hashlib
import json

def verify_activation_signature(payload, signature, secret_token):
    expected_signature = hmac.new(
        secret_token.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(signature, expected_signature)

# Example usage
payload = json.dumps({
    "event": "webhook.activation",
    "processed_at": "2025-01-15T10:30:00Z"
})
is_valid = verify_activation_signature(
    payload,
    "abc123def456...",
    "your_webhook_secret_token"
)
PHP
<?php
function verifyActivationSignature($payload, $signature, $secretToken) {
    $expectedSignature = hash_hmac('sha256', $payload, $secretToken);
    return hash_equals($expectedSignature, $signature);
}

// Example usage
$payload = json_encode([
    'event' => 'webhook.activation',
    'processed_at' => '2025-01-15T10:30:00Z'
]);
$isValid = verifyActivationSignature(
    $payload,
    'abc123def456...',
    'your_webhook_secret_token'
);
?>
Ruby
require 'openssl'
require 'json'

def verify_activation_signature(payload, signature, secret_token)
  expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_token, payload)
  ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
end

# Example usage
payload = {
  event: "webhook.activation",
  processed_at: "2025-01-15T10:30:00Z"
}.to_json
is_valid = verify_activation_signature(
  payload,
  "abc123def456...",
  "your_webhook_secret_token"
)
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

public boolean verifyActivationSignature(String payload, String signature, String secretToken) {
    try {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretToken.getBytes(), "HmacSHA256");
        mac.init(secretKeySpec);
        
        byte[] expectedSignature = mac.doFinal(payload.getBytes());
        byte[] providedSignature = hexStringToByteArray(signature);
        
        return Arrays.equals(expectedSignature, providedSignature);
    } catch (NoSuchAlgorithmException | InvalidKeyException e) {
        return false;
    }
}

private byte[] hexStringToByteArray(String s) {
    int len = s.length();
    byte[] data = new byte[len / 2];
    for (int i = 0; i < len; i += 2) {
        data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                             + Character.digit(s.charAt(i+1), 16));
    }
    return data;
}
C#
using System;
using System.Security.Cryptography;
using System.Text;

public bool VerifyActivationSignature(string payload, string signature, string secretToken)
{
    using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretToken)))
    {
        byte[] expectedSignature = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
        byte[] providedSignature = HexStringToByteArray(signature);
        
        return CryptographicOperations.FixedTimeEquals(expectedSignature, providedSignature);
    }
}

private byte[] HexStringToByteArray(string hex)
{
    int numberChars = hex.Length;
    byte[] bytes = new byte[numberChars / 2];
    for (int i = 0; i < numberChars; i += 2)
    {
        bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
    }
    return bytes;
}
Go
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
)

func verifyActivationSignature(payload, signature, secretToken string) bool {
    h := hmac.New(sha256.New, []byte(secretToken))
    h.Write([]byte(payload))
    expectedSignature := hex.EncodeToString(h.Sum(nil))
    
    return hmac.Equal([]byte(signature), []byte(expectedSignature))
}

// Example usage
payload := `{"event":"webhook.activation","processed_at":"2025-01-15T10:30:00Z"}`
isValid := verifyActivationSignature(
    payload,
    "abc123def456...",
    "your_webhook_secret_token",
)

Response Required

Your endpoint should return HTTP 200-299 to activate the webhook:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "status": "success",
  "message": "Webhook activated successfully"
}

Step 2: Create Operations

Now you can create cash-in and cash-out operations for your users.

Create a Cash-In Operation

curl -X POST "https://your-domain.com/api/v1/cash/cash_in" \
  -H "Content-Type: application/json" \
  -H "X-Client-Id: a1b2c3d4e5f6g7h8" \
  -H "X-Signature: abc123def456..." \
  -H "X-Timestamp: 1705312200" \
  -d '{
    "amount": 500.00,
    "external_user_id": "USER123456",
    "document_type": "INE",
    "document_id": "1234567890123",
    "phone": "5512345678"
  }'

Create a Cash-Out Operation

curl -X POST "https://your-domain.com/api/v1/cash/cash_out" \
  -H "Content-Type: application/json" \
  -H "X-Client-Id: a1b2c3d4e5f6g7h8" \
  -H "X-Signature: abc123def456..." \
  -H "X-Timestamp: 1705312200" \
  -d '{
    "amount": 250.00,
    "external_user_id": "USER789012",
    "document_type": "CURP",
    "document_id": "ABCD123456HMNMNL01",
    "phone": "5587654321"
  }'

Response

{
  "operation_id": 123,
  "kind": "cash_in",
  "reference": "10511175512161627448",
  "status": "unpaid",
  "transaction_id": "FMXdbnBuiw2SHqSyfzSkqN71q",
  "amount": 500.0,
  "created_at": "2025-01-15T10:30:00Z",
  "expire_at": "2025-01-18T10:30:00Z"
}

Important: Store the reference number. Users will need it to complete the operation at physical locations.

Step 3: Monitor Operation Status

Check the status of operations using the reference number.

Consult Operation

curl -X GET "https://your-domain.com/api/v1/cash/consult?reference=10511175512161627448" \
  -H "X-Client-Id: a1b2c3d4e5f6g7h8" \
  -H "X-Signature: abc123def456..." \
  -H "X-Timestamp: 1705312200"

Response

{
  "operation_id": 123,
  "kind": "cash_in",
  "reference": "10511175512161627448",
  "status": "paid",
  "transaction_id": "FMXdbnBuiw2SHqSyfzSkqN71q",
  "amount": 500.0,
  "created_at": "2025-01-15T10:30:00Z",
  "expire_at": "2025-01-18T10:30:00Z"
}

Step 4: Handle Webhook Notifications

Your webhook endpoint will receive notifications when operations change status. These are incoming webhooks that Monato sends to your server - you don't make API calls to receive them.

Webhook Events

Monato will send your webhook endpoint two types of events:

  1. Webhook Activation Test - Sent when you first configure a webhook to verify your endpoint works
  2. Operation Status Updates - Sent when cash operations change status (paid, expired, reversed)

Webhook Payload Example

{
  "event": "webhook.paid.success",
  "operation_id": 123,
  "external_user_id": "USER789012",
  "type": "cash_in",
  "status": "paid",
  "amount": 500,
  "reference": "10511175512161627448",
  "processed_at": "2025-01-15T10:30:00Z"
}

Verify Webhook Signatures

Always verify webhook signatures to ensure authenticity. Select your programming language:

JavaScript/Node.js
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );
}
Python
import hmac
import hashlib

def verify_webhook_signature(payload, signature, secret):
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(signature, expected_signature)
PHP
<?php
function verifyWebhookSignature($payload, $signature, $secret) {
    $expectedSignature = hash_hmac('sha256', $payload, $secret);
    return hash_equals($expectedSignature, $signature);
}
?>
Ruby
require 'openssl'

def verify_webhook_signature(payload, signature, secret)
  expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret, payload)
  ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
end
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

public boolean verifyWebhookSignature(String payload, String signature, String secret) {
    try {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
        mac.init(secretKeySpec);
        
        byte[] expectedSignature = mac.doFinal(payload.getBytes());
        byte[] providedSignature = hexStringToByteArray(signature);
        
        return Arrays.equals(expectedSignature, providedSignature);
    } catch (NoSuchAlgorithmException | InvalidKeyException e) {
        return false;
    }
}

private byte[] hexStringToByteArray(String s) {
    int len = s.length();
    byte[] data = new byte[len / 2];
    for (int i = 0; i < len; i += 2) {
        data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                             + Character.digit(s.charAt(i+1), 16));
    }
    return data;
}
C#
using System;
using System.Security.Cryptography;
using System.Text;

public bool VerifyWebhookSignature(string payload, string signature, string secret)
{
    using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
    {
        byte[] expectedSignature = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
        byte[] providedSignature = HexStringToByteArray(signature);
        
        return CryptographicOperations.FixedTimeEquals(expectedSignature, providedSignature);
    }
}

private byte[] HexStringToByteArray(string hex)
{
    int numberChars = hex.Length;
    byte[] bytes = new byte[numberChars / 2];
    for (int i = 0; i < numberChars; i += 2)
    {
        bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
    }
    return bytes;
}
Go
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

func verifyWebhookSignature(payload, signature, secret string) bool {
    h := hmac.New(sha256.New, []byte(secret))
    h.Write([]byte(payload))
    expectedSignature := hex.EncodeToString(h.Sum(nil))
    
    return hmac.Equal([]byte(signature), []byte(expectedSignature))
}

Business Rules to Remember

Amount Limits

  • Cash-In: Maximum amount varies by physical location
  • Cash-Out: Maximum amount varies by physical location

User Limits

  • Daily Limit: Maximum 5 operations per user per day
  • Reset Time: Limits reset at midnight (Mexico City timezone)

Document Validation

  • INE: 13 digits (e.g., 1234567890123)
  • CURP: 18 characters (e.g., ABCD123456HMNMNL01)
  • RFC: 10-13 characters (e.g., ABCD123456ABC)
  • Phone: Exactly 10 digits (e.g., 5512345678)

Expiration Policy

  • Cash-In Operations: Expire 3 days after creation.
    • Open References: Expire based on the custom date you set during creation.
  • Cash-Out Operations: Expire 60 minutes after creation by default, but can be configured by the client to last longer
  • Expired operations cannot be completed
  • Monitor expire_at timestamp in responses

Error Handling

The API returns specific error codes for different scenarios:

Error CodeDescriptionResolution
60Invalid parametersCheck request parameters and validation rules
61Invalid document identifierVerify document type and ID format
13Invalid reference lengthReference must be exactly 20 digits
14Invalid referenceReference not found in system
64Operation not foundVerify reference number exists

Conciliation

File structure

  • Field delimiter: pipe character | (vertical bar).
  • Order: exactly one header row first, zero or more body rows, exactly one footer row last.

Each logical row is a single text line. Fields must not contain the delimiter unless an escaping rule is agreed separately (default: no pipe characters inside fields).

Header row (h)

The header row marks the beginning of the reconciliation file.

Format

h
  • The line consists of the single character h (lowercase) with no additional fields.

Body row (b)

Each body row represents a single reconciled transaction (cash-in or cash-out).

Format

b|<payment_id>|<type>|<amount>|<reference>|<operation_date>|<currency>
PositionFieldDescriptionFormat / notes
1Row markerLiteral b (lowercase).Fixed.
2payment_idUnique identifier of the payment / transaction.UUID (canonical string form, lowercase hex with hyphens, e.g. 42dd06f4-c53d-4951-95ba-0ce5e8c35f30).
3typeOperation kind.CASH_IN for cash-in, CASH_OUT for cash-out.
4amountTransaction amount.Decimal, dot as separator, two fractional digits recommended (e.g. 2495.00, 123.00).
5referenceReference or destination account for the movement.String (e.g. 20-digit reference). No spaces unless part of the agreed reference format.
6operation_dateOperation date.ddmmyyyy (day two digits, month two digits, year four digits). Example: 25 March 2026 → 25032026.
7currencyTransaction currency.ISO 4217 alphabetic code (e.g. MXN).

Example body lines

b|42dd06f4-c53d-4951-95ba-0ce5e8c35f30|CASH_OUT|2495.00|20511446630069045555|25032026|MXN
b|49154917-b77e-4af0-8155-eec9b45df262|CASH_OUT|123.00|20511446636424426786|25032026|MXN
b|104ca5a9-277e-4d6b-b5f8-31a68a9a5270|CASH_OUT|948.00|20511446637894288912|25032026|MXN

Cash-in rows use the same structure with CASH_IN (or the agreed cash_in literal) in the type field.

The footer row carries totals used to validate the file against the body rows.

Format

f|<total_payments>|<total_amount>
PositionFieldDescriptionFormat / notes
1Row markerLiteral f (lowercase).Fixed.
2total_paymentsCount of body rows in the file.Non-negative integer (e.g. 3).
3total_amountSum of all body row amounts.Decimal, same rules as body amount (e.g. 3566.00). Must equal the arithmetic sum of the amount fields of all b rows (to the agreed rounding rule).

Example

f|3|3566.00

Cash-in vs Cash-out

  • Single format: Cash-in and cash-out share the same file layout, delimiter, and columns.
  • Discrimination: The body field type indicates whether the row is cash-in or cash-out.
  • Mixed files: A file may contain both CASH_IN and CASH_OUT rows when batching a calendar period or settlement window.

Naming convention

Recommended pattern (adjust PREFIX with your integration client name):

{PREFIX}_CASH_{ddmmyyyy}.txt
PartMeaning
PREFIXClient name prefix (e.g. client short code).
CASHFixed segment indicating cash reconciliation.
ddmmyyyyBusiness or settlement date in ddmmyyyy form, aligned with the reconciliation period.

Example: ACME_CASH_25032026.txt

Some pipelines use the .csv extension for the same content; the format is still delimiter-separated text, not a comma-separated CSV. Use the extension agreed with operations.

Full file example

h
b|42dd06f4-c53d-4951-95ba-0ce5e8c35f30|OUT|2495.00|20511446630069045555|25032026|MXN
b|49154917-b77e-4af0-8155-eec9b45df262|OUT|123.00|20511446636424426786|25032026|MXN
b|104ca5a9-277e-4d6b-b5f8-31a68a9a5270|OUT|948.00|20511446637894288912|25032026|MXN
f|3|3566.00

Checks: total_payments = 3; total_amount = 2495.00 + 123.00 + 948.00 = 3566.00.

Row types

Row markerRole
hHeader — identifies the start of the file.
bBody — one completed payment / movement.
fFooter — aggregate totals for validation.

Next Steps

  1. Test Your Integration: Use the staging environment to test your implementation
  2. Implement Error Handling: Add proper error handling for all response codes
  3. Monitor Webhooks: Ensure your webhook endpoint is reliable and responds quickly
  4. Handle Edge Cases: Implement logic for expired operations and daily limits

Support

Need help with your integration?


Happy coding with Monato Cash API!