Maya Saved Cards
How Batchmates vaults card details with Maya and charges them later — without redirecting the user to a hosted page.
Maya's vaulting flow is browser-first: card data is tokenized directly in the browser against Maya's API. Batchmates servers never see raw card numbers.
Card Vaulting Flow
Step 1 — Tokenize in the browser
The frontend POSTs card details directly to Maya's tokenization endpoint using the public key (Basic Auth):
POST https://pg-sandbox.paymaya.com/payments/v1/payment-tokens
Authorization: Basic base64(MAYA_PUBLIC_KEY:)
Request
{
"card": {
"number": "4123450131001381",
"expMonth": "12",
"expYear": "2028",
"cvc": "123",
"name": "Juan Dela Cruz"
}
}
Maya returns a paymentTokenId — a short-lived token representing the card. No raw card data ever reaches Batchmates.
Step 2 — Vault via backend
Endpoint: POST /api/v1/payment-methods/maya/link-card
Authentication: Required
Rate limit: 5 requests per minute
Send the paymentTokenId plus card metadata to the backend. The backend creates a Maya customer for the user (or reuses an existing one) and calls Maya's vault API.
- Name
payment_token_id- Type
- string
- Description
The
paymentTokenIdreturned from Maya's tokenization endpoint
- Name
card_meta- Type
- object
- Description
Card metadata from the tokenization step
- Name
card_meta.last4- Type
- string
- Description
Last 4 digits of the card
- Name
card_meta.brand- Type
- string
- Description
Card brand (e.g.
"visa","mastercard")
- Name
card_meta.exp_month- Type
- integer
- Description
Expiration month (1–12)
- Name
card_meta.exp_year- Type
- integer
- Description
Expiration year (4 digits)
Request
{
"payment_token_id": "8iDnJOw10fA6J6HR...",
"card_meta": {
"last4": "1381",
"brand": "visa",
"exp_month": 12,
"exp_year": 2028
}
}
Response
{
"success": true,
"data": {
"id": 12,
"payment_gateway": "maya",
"card_last_four": "1381",
"card_brand": "visa",
"is_default": true,
"is_vaulted": true
},
"message": "Card linked successfully"
}
Internal Vaulting Sequence
// app/Services/MayaTransactionService.php
public function linkCard(int $userId, string $paymentTokenId, array $cardMeta): PaymentMethod
{
// Reuse existing Maya customer or create new one
$customerId = $existingCustomerId ?? $this->maya->createCustomer([...])['id'];
// Vault the token → returns cardTokenId (long string)
$card = $this->maya->linkCard($customerId, $paymentTokenId, $refNumber);
$cardTokenId = $card['cardTokenId'];
return PaymentMethod::create([
'payment_gateway' => 'maya',
'gateway_token' => $cardTokenId, // long card token
'gateway_customer_id' => $customerId, // Maya customer UUID
...
]);
}
One Maya customer record is created per user (keyed by email). When a user adds their second Maya card, the existing customer ID is reused.
Charging a Saved Maya Card
Charges a saved Maya card directly, bypassing hosted checkout.
Authentication: Required
Rate limit: 5 requests per minute
Request Body
- Name
campaign_id- Type
- integer
- Description
ID of the campaign to donate to
- Name
payment_method_id- Type
- integer
- Description
ID of the saved Maya payment method. Must belong to the authenticated user and have
payment_gateway = "maya".
- Name
amount- Type
- number
- Description
Donation amount in PHP (minimum: 1)
- Name
is_anonymous- Type
- boolean
- Description
Make donation anonymous. Default:
false
- Name
message- Type
- string
- Description
Optional message to the campaign (max 500 characters)
Request
{
"campaign_id": 1,
"payment_method_id": 12,
"amount": 500
}
Response (completed)
{
"success": true,
"data": {
"id": 88,
"amount": "500.00",
"status": "completed",
"payment_gateway": "maya",
"paid_at": "2025-03-01T08:00:00.000000Z"
},
"message": "Donation charged successfully"
}
Response (3DS required)
{
"success": false,
"requires_action": true,
"action_url": "https://payments-web-sandbox.maya.ph/authenticate?id=...",
"message": "Payment requires authentication"
}
3DS Authentication
Maya cards start in PREVERIFICATION state. The first charge triggers a 3DS authentication flow. After successful verification the card charges freely without further 3DS prompts.
When requires_action: true is returned:
- Redirect the user to
action_url - Maya presents the 3DS challenge
- After authentication, Maya redirects back to
/api/v1/payments/maya/success - The backend verifies the payment with Maya's API and marks the donation
completed
Vaulted Card Status
All PaymentMethod records expose an is_vaulted computed field — true when both gateway_customer_id and gateway_token are present. The raw token values are never returned in API responses.
What Cannot Be Vaulted
GCash and Maya wallet payments cannot be saved as reusable payment methods. Each payment requires a fresh authorization. Use Maya Checkout for these payment types.
Only credit/debit cards support vaulting.
Mobile / Native Clients
The vaulting and charge endpoints are platform-neutral, but the card-capture step shown here is written for the web. On native clients:
- Tokenization has no browser dependency. Call Maya's
POST /payments/v1/payment-tokensdirectly from the app (or use Maya's mobile SDK) with the public key to obtain thepaymentTokenId, then send it tolink-card. Raw card data still never touches Batchmates servers — the same security guarantee holds on mobile. - 3DS is a browser round-trip. When
charge-saved/mayareturnsrequires_actionwith anaction_url, open it in an in-app browser (ASWebAuthenticationSession/ Custom Tabs), not an embeddedWebView. - The success redirect resolves to the web
FRONTEND_URL— register it as a deep link / universal link so the app regains control after the 3DS challenge, then re-fetch the donation to confirm the final state. - The first charge on a
PREVERIFICATIONcard triggers 3DS; subsequent charges do not — so the browser round-trip is usually a one-time cost per card, unlike PayMongo.