Skip to main content

Overview

MechZie uses Razorpay for payment collection and RazorpayX for mechanic payouts. Every monetary operation is backed by an immutable double-entry ledger — the payments table records Razorpay state, while ledger_entries is the financial source of truth. All amounts are in paisa (₹1 = 100 paisa).

Architecture

Payment Flow

Two paths confirm a payment — the optimistic verify (fast UI) and the webhook (canonical truth). Both are idempotent; if they race, the first one wins.

Step-by-Step Integration

1

Create a Razorpay Order

After the mechanic marks a job as completed, the customer creates a payment order:
curl -X POST https://api.mechzie.com/api/v1/payments/create-order \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "job_id": "uuid-of-completed-job" }'
Response (200):
{
  "orderId": "order_PqR1s2T3u4V5",
  "amount": 45000,
  "currency": "INR"
}
Amount is in paisa. Display as ₹${(amount / 100).toStringAsFixed(0)}.
2

Open Razorpay Checkout

Use the Razorpay Flutter SDK:
final razorpay = Razorpay();

razorpay.on(Razorpay.EVENT_PAYMENT_SUCCESS, (PaymentSuccessResponse response) {
  verifyPayment(response.orderId!, response.paymentId!, response.signature!);
});
razorpay.on(Razorpay.EVENT_PAYMENT_ERROR, (PaymentFailureResponse response) {
  showPaymentError(response.message);
});

razorpay.open({
  'key': 'rzp_live_YOUR_KEY_ID',
  'amount': 45000,
  'currency': 'INR',
  'order_id': 'order_PqR1s2T3u4V5',
  'name': 'MechZie',
  'description': 'Service Payment',
  'prefill': { 'contact': '+919876543210' },
});
Use the Razorpay Key ID (starts with rzp_), not the Key Secret. The Key Secret is server-side only.
3

Verify Payment (Optimistic)

After Razorpay returns success, verify the signature:
curl -X POST https://api.mechzie.com/api/v1/payments/verify \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "razorpay_order_id": "order_PqR1s2T3u4V5",
    "razorpay_payment_id": "pay_AbC1d2E3f4G5",
    "razorpay_signature": "hmac-sha256-signature-string"
  }'
On success, this also triggers the commission split into the ledger (mechanic net earnings + platform commission). The webhook is the canonical fallback if this path fails.
If verify and webhook disagree, webhook always wins. The ledger uses idempotency keys to prevent double-processing.

Commission Split

When a payment is captured, CommissionService splits the gross amount:
ComponentFormulaExample (₹450 job)
GrossPayment amount45000 paisa
Commissiongross × rate_bps / 100009000 paisa (20%)
Net earningsgross − commission36000 paisa
The commission rate is dynamic — stored in platform_config and changeable without redeployment.

Webhook Processing

The webhook pipeline guarantees exactly-once processing via payment_events.razorpay_event_id: Handled events:
Razorpay EventAction
payment.capturedPayment → captured, job → paid, commission ledger entries
payment.failedPayment → failed
refund.processedRefund row → processed, payment → refunded
payout.processedPayout → processed, settlement → completed
payout.failedPayout → failed, settlement → failed
payout.reversedPayout → reversed
Endpoint: POST /api/v1/payments/webhook
  • No authentication (Razorpay sends directly)
  • Verified via HMAC-SHA256 in X-Razorpay-Signature header
  • Always returns 200 to prevent Razorpay retries
Frontend devs don’t need to implement webhook handling — it’s server-side only. But you should handle the case where verify succeeds but the UI should wait for the job status to update to paid.

Checking Payment Status

GET /api/v1/payments/{jobId}
Authorization: Bearer ACCESS_TOKEN
Response:
{
  "id": "uuid",
  "job_id": "uuid",
  "type": "service",
  "razorpay_order_id": "order_PqR1s2T3u4V5",
  "razorpay_payment_id": "pay_AbC1d2E3f4G5",
  "amount": 45000,
  "status": "captured",
  "method": "upi",
  "created_at": "2026-05-21T10:30:00.000Z"
}

Payment Statuses

StatusMeaningUI Action
createdOrder created, waiting for paymentShow checkout button
authorizedPayment authorized (rare intermediate)Show “Processing…”
capturedPayment successfulShow receipt ✅
failedPayment failedShow retry option
refundedRefund processedShow refund confirmation

Cancellation Fees

Late cancellations incur fees. The fee amount is configurable in platform_config.

Customer cancellation

Cancelling from en_route, arrived, or in_progress → flat ₹50 fee (default). The customer cannot create new jobs until the fee is paid (402 PAYMENT_REQUIRED).

Mechanic cancellation

Consecutive cancellations above a threshold (default: 3 in 30 days) → ₹100 penalty deducted from their wallet automatically.

Check for Unpaid Fees

GET /api/v1/payments/unpaid-fees
Authorization: Bearer ACCESS_TOKEN
{ "hasFees": true, "amount": 5000 }
Use the same create-order → Razorpay checkout → verify flow with type: "cancellation_fee".
Customers with unpaid fees cannot create new jobs (402 PAYMENT_REQUIRED). Always check on app launch and prompt payment if needed.

Pricing Model

ComponentDescriptionExample
base_priceSet per service category (in paisa)35000 (₹350)
Line itemsParts + labor added by mechanic10000 (₹100)
final_pricebase_price + SUM(line items)45000 (₹450)
CommissionPlatform cut (configurable BPS)9000 (₹90 at 20%)
Net to mechanicfinal_price − commission36000 (₹360)
All amounts in the API are in paisa. Always divide by 100 for display: ₹${(amount / 100).toStringAsFixed(0)}.

Client SDKs Required

SDKPackagePurpose
Razorpay Flutterrazorpay_flutterPayment checkout UI
The Razorpay Key ID will be provided via your app’s environment configuration. Do not hardcode it.