Saved Cards

How Batchmates vaults card details with Magpie and charges them later — without redirecting the user to a hosted page.


Why Customers Exist

Magpie issues two kinds of objects when you tokenize a card:

ObjectID prefixLifetime
Sourcesrc_xxxCan expire on its own
Customercus_xxxPermanent

A bare src_xxx source can expire before you charge it. Attaching it to a cus_xxx customer makes the card permanently vaulted — the source lives as long as the customer does.


Saving a Card — Two-Step Tokenization

Card data never touches the backend directly. The flow is split into two requests so that raw card numbers and CVCs only transit the Magpie tokenization endpoint and are never stored or logged by Batchmates.

Step 1 — Tokenize the card

Endpoint: POST /api/v1/payments/magpie/create-source

Send the raw card fields to this endpoint. The backend forwards them to Magpie and returns the resulting source_id plus the card metadata that Magpie echoes back (last4, brand, exp).

Request Body

  • Name
    number
    Type
    string
    Description

    Full card number (16 digits, no spaces)

  • Name
    exp_month
    Type
    integer
    Description

    Expiration month (1–12)

  • Name
    exp_year
    Type
    integer
    Description

    Expiration year (4 digits, e.g. 2028)

  • Name
    cvc
    Type
    string
    Description

    Card security code (3–4 digits)

  • Name
    name
    Type
    string
    Description

    Cardholder name as it appears on the card

Request

{
  "number": "4242424242424242",
  "exp_month": 12,
  "exp_year": 2028,
  "cvc": "123",
  "name": "Juan Dela Cruz"
}

Response

{
  "success": true,
  "data": {
    "id": "src_abc123xyz",
    "type": "card",
    "card": {
      "last4": "4242",
      "brand": "visa",
      "exp_month": 12,
      "exp_year": 2028
    }
  }
}

Step 2 — Vault the source

Endpoint: POST /api/v1/payment-methods

Send only the source_id returned in Step 1, plus the card metadata from the source response. No raw card data is accepted here.

Request Body

  • Name
    payment_gateway
    Type
    string
    Description

    Must be "magpie"

  • Name
    source_id
    Type
    string
    Description

    The id returned from create-source (e.g. src_abc123xyz)

  • Name
    card_last_four
    Type
    string
    Description

    Last 4 digits from the source response (exactly 4 characters)

  • Name
    card_brand
    Type
    string
    Description

    Card brand from the source response (e.g. "visa", "mastercard")

  • Name
    card_exp_month
    Type
    integer
    Description

    Expiration month from the source response

  • Name
    card_exp_year
    Type
    integer
    Description

    Expiration year from the source response

  • Name
    set_as_default
    Type
    boolean
    Description

    Set as default payment method. Default: false

Request

{
  "payment_gateway": "magpie",
  "source_id": "src_abc123xyz",
  "card_last_four": "4242",
  "card_brand": "visa",
  "card_exp_month": 12,
  "card_exp_year": 2028
}

Response

{
  "success": true,
  "data": {
    "id": 3,
    "payment_gateway": "magpie",
    "card_last_four": "4242",
    "card_brand": "visa",
    "card_exp_month": 12,
    "card_exp_year": 2028,
    "is_default": false
  },
  "message": "Payment method added successfully"
}

Vaulting Sequence (Internal)

When POST /api/v1/payment-methods is called with a source_id, MagpieTransactionService::savePaymentMethodFromSource() runs this sequence:

// app/Services/MagpieTransactionService.php

public function savePaymentMethodFromSource(int $userId, string $sourceId, array $cardMeta): PaymentMethod
{
    $user = User::findOrFail($userId);

    // Step 1 — get or create a Magpie customer → cus_xxx
    $existingCustomerId = PaymentMethod::where('user_id', $userId)
        ->whereNotNull('gateway_customer_id')
        ->value('gateway_customer_id');

    $customerId = $existingCustomerId ?? $this->magpieService->createCustomer([
        'email'       => $user->email,
        'description' => $user->name,
    ])['id'];

    // Step 2 — attach source to customer (vaults the card)
    $this->magpieService->attachSourceToCustomer($customerId, $sourceId);

    // Step 3 — persist the PaymentMethod record (no raw card data stored)
    return PaymentMethod::create([
        'user_id'             => $user->id,
        'payment_gateway'     => 'magpie',
        'gateway_token'       => $sourceId,       // src_xxx
        'gateway_customer_id' => $customerId,     // cus_xxx
        'card_last_four'      => $cardMeta['last4'],
        'card_brand'          => $cardMeta['brand'],
        'card_exp_month'      => $cardMeta['exp_month'],
        'card_exp_year'       => $cardMeta['exp_year'],
    ]);
}

Charging a Saved Card

Endpoint: POST /api/v1/donations/charge-saved

No browser redirect is needed. The charge is synchronous from the backend.

Request Body

  • Name
    campaign_id
    Type
    integer
    Description

    Campaign to donate to

  • Name
    payment_method_id
    Type
    integer
    Description

    ID of the saved card. Must belong to the authenticated user — returns 403 otherwise.

  • Name
    amount
    Type
    number
    Description

    Donation amount in PHP (minimum: 1)

  • Name
    is_anonymous
    Type
    boolean
    Description

    Default: false

  • Name
    message
    Type
    string
    Description

    Optional message (max 500 characters)

Request

{
  "campaign_id": 1,
  "payment_method_id": 3,
  "amount": 500
}

Response

{
  "success": true,
  "data": {
    "id": 91,
    "amount": "500.00",
    "status": "completed",
    "payment_gateway": "magpie",
    "paid_at": "2026-03-01T08:00:00.000000Z"
  },
  "message": "Donation charged successfully"
}

Error (Wrong Owner — 403)

{
  "success": false,
  "message": "Payment method does not belong to you"
}

Internal Charge Call

// app/Services/MagpieTransactionService.php

public function chargeWithSavedPaymentMethod(Donation $donation, PaymentMethod $pm): void
{
    $fees = FeeCalculator::calculate('magpie', $donation->amount);

    $charge = $this->magpieService->createCharge([
        'customer' => $pm->gateway_customer_id,  // cus_xxx
        'source'   => $pm->gateway_token,         // src_xxx
        'amount'   => (int) round($fees['total_amount'] * 100),
        'currency' => 'php',
        'metadata' => [
            'reference_number' => $donation->reference_number,
            'donation_id'      => $donation->id,
        ],
    ]);

    // If charge immediately succeeds, donation is marked completed synchronously.
    // If the charge is pending (e.g. requires 3DS), the charge.succeeded webhook finalises it.
    if ($charge['status'] === 'succeeded') {
        $donation->update(['status' => 'completed', 'paid_at' => now()]);
        $campaign->increment('raised_amount', $donation->amount);
        $campaign->increment('available_amount', $donation->amount);
        $campaign->increment('supporter_count');
    }
}

If the charge returns status: 'succeeded' synchronously, the donation is marked completed immediately — no webhook wait needed. If the charge requires 3DS authentication, an action.url is returned and the charge.succeeded webhook finalises the donation after the user completes authentication.


Vaulting Status

The POST /api/v1/payment-methods endpoint and savePaymentMethodFromSource service method are fully implemented. Once Magpie enables the vaulting feature on the account, no code changes are needed.


What Cannot Be Vaulted

GCash, Maya, and QR Ph sources are redirect-based and single-use. They cannot be attached to a customer and reused. Only credit/debit card sources (type: 'card') support vaulting.

Was this page helpful?