Appearance
Step 4: Create Payment
Create one collection attempt for an unpaid order, using the customer-confirmed total.
Save data.payment_id from the create-payment response (HTTP 201 or 202). Use it to call GET /v2/payments/{payment_id} and track the result. Do not create another payment just to check its status.
Before creating a payment
Call Calculate Order before each new payment attempt and confirm the returned total with the customer. This picks up changes to fees or the selected payment method. Send that total as expected_payment_total.
Endpoint
http
POST https://api.iimmpact.com/v2/paymentsRequest headers
| Field | Type | Required | Description |
|---|---|---|---|
X-Api-Key | string | Yes | Your API key; keep it on your backend. |
X-Timestamp | string | Yes | Current Unix time in seconds; within five minutes of server time. |
X-Nonce | string | Yes | Fresh identifier for every request, including retries. |
X-Signature | string | Yes | v1= followed by the request's HMAC signature. |
Follow API Key Authentication. Sign the exact body, or an empty body for bodyless requests, and the sorted query when present.
| Field | Type | Required | Description |
|---|---|---|---|
Content-Type | string | Yes | application/json. |
Idempotency-Key | string | Yes | Unique key for this payment; reuse it for network retries. |
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
order_id | string | Yes | Existing unpaid order, with no pending or unresolved payment. |
customer | object | Yes | Customer details. See customer profile. |
customer.external_id | string | Yes | Your customer ID. Use the same ID for payment-method lookup. |
customer.name | string | For new customers | Customer's name. Omit if already provided. |
customer.email | string | No | Customer's email address, ASCII, up to 320 characters. |
payment_method_code | string | Yes | Code from Payment Methods. |
return_url | string | Yes | Your HTTPS destination after checkout or authentication. See restrictions below. |
expected_payment_total | string | Yes | Positive two-place MYR total confirmed by the customer. |
customer_fee_percent | string | No | Percentage of the fee the customer bears, 0.00–100.00; defaults to 0.00 on each request. |
payment_method_details | object | Conditional | Required for FPX (option_code) and new CARD (flat card fields). Not required when using a saved card. |
save_payment_method | boolean | No | Top-level saving request with customer consent; defaults to false. Applies to new CARD, subject to capability activation. |
saved_payment_method_id | string | No | Top-level saved-card selection for this customer and account. Do not combine with a new card number or save_payment_method: true. |
return_url cannot contain userinfo, fragments, local/private destinations, or existing order_id/payment_id query keys, including encoded or case-insensitive variants. Other safe query parameters are retained. Use a destination you control and authorize order access on your backend.
Customer profile
Send customer details once. For later payments, send only:
json
{ "customer": { "external_id": "CUS-1042" } }Existing customer details are reused, not overwritten. Supplied details fill missing values.
Choose payment input
| Choice | Send | Customer action |
|---|---|---|
| Personal FPX | payment_method_details.option_code from method options | Choose a bank in your UI, then follow the bank redirect. |
| Saved CARD | Top-level saved_payment_method_id; omit payment_method_details | Complete any required authentication. |
| Direct new CARD | Flat card fields inside payment_method_details, only when enabled | Enter details in your UI, then complete any required 3DS. |
Use a saved card returned for the same customer and method. To save a new card, set save_payment_method: true with customer consent. Do not combine a saved card with new card details or a saving request.
Request example — personal FPX
json
{
"order_id": "ord_example",
"customer": {
"external_id": "CUS-1042",
"name": "Aisha Rahman",
"email": "aisha@example.com"
},
"payment_method_code": "FPX",
"return_url": "https://shop.example.com/payment-return",
"customer_fee_percent": "50.00",
"expected_payment_total": "101.00",
"payment_method_details": {
"option_code": "CIMB_FPX"
}
}Use the actual calculated total, including customer fees. Personal FPX supports MYR 1.00–30,000.00, inclusive; business FPX is not supported. No bank credentials or bank-account numbers are collected by this API.
Request example — saved CARD
Select payment_method_code: "CARD", recalculate its total, and send the saved-method ID at the top level:
json
{
"order_id": "ord_example",
"customer": { "external_id": "CUS-1042" },
"payment_method_code": "CARD",
"return_url": "https://shop.example.com/payment-return",
"customer_fee_percent": "50.00",
"expected_payment_total": "100.85",
"saved_payment_method_id": "01J9Z6Q2W8X4K7M3N5P1R2S3T4"
}New card and cardholder details
Activation required
Direct full-PAN entry is a gated capability. It requires PCI-DSS Level 1 compliance and account approval. Setting save_payment_method: true requests saving; a saved method becomes usable only after verified success.
Your card form sends input through your backend → Pine Labs. Keep API/HMAC credentials off the browser. Systems handling card input are in PCI scope: never log or durably store card-bearing payment_method_details, including in retry queues or request capture. Never retain CVV after authorization.
These are the card fields directly within payment_method_details, subject to activation validation:
| Field | Type | Required | Description |
|---|---|---|---|
card_number | string | Yes | Full PAN, digits only. |
expiry_month | string | Yes | Two-digit month, 01–12. |
expiry_year | string | Yes | Four-digit year. |
cvv | string | Yes | Card security code, digits only. |
cardholder_first_name | string | Yes | Cardholder's first name. |
cardholder_last_name | string | Yes | Cardholder's last name. |
cardholder_email | string | Yes | Valid cardholder email. |
cardholder_phone_number | string | Yes | Cardholder phone in E.164 format, for example +60123456789. |
Collect the cardholder's first name, last name, email and phone when entering a new card, and send them inside payment_method_details. The customer profile does not replace these cardholder fields. Saved-card payments use saved_payment_method_id and do not require the cardholder fields again.
Example using a test card:
json
{
"order_id": "ord_example",
"customer": { "external_id": "CUS-1042" },
"payment_method_code": "CARD",
"return_url": "https://shop.example.com/payment-return",
"customer_fee_percent": "50.00",
"expected_payment_total": "100.85",
"save_payment_method": false,
"payment_method_details": {
"card_number": "4000000000001091",
"expiry_month": "12",
"expiry_year": "2029",
"cvv": "123",
"cardholder_first_name": "Amira",
"cardholder_last_name": "Hassan",
"cardholder_email": "amira@example.com",
"cardholder_phone_number": "+60123456789"
}
}Retry after a lost response: reuse the same key and customer object. For CARD, omit card-bearing payment_method_details; for FPX, retain the same payment_method_details.option_code. Once creation has resolved, its original response is returned. While creation is still in flight, 409 payment_creation_in_progress returns error.order_id and error.payment_id; use the payment ID to read the existing attempt. If fresh card input is required, collect it again securely; do not use stored card data.
Response fields
Fields below are inside data.
| Field | Type | Description |
|---|---|---|
payment_id | string | Save for status checks. |
order_id | string | Order being paid. |
external_reference | string | Your order reference. |
customer_external_id | string | Your customer ID for this payment. |
transaction_refids | string[] | Fulfillment references for the successful settlement winner. Empty before settlement and for duplicate collections that do not own fulfillment. These references do not imply fulfillment success. |
status | string | Current collection state. |
currency | string | MYR. |
payment_method_code | string | Selected method. |
customer_fee_percent | string | Percentage of the fee the customer bears, 0.00–100.00. |
fee | object | Fee rule used for this payment. |
totals | object | Frozen amount breakdown. |
action | object | Present when customer action is required; otherwise omitted. |
action.type | string | redirect, when action exists. |
action.url | string | HTTPS URL for the customer to complete payment or authentication. |
action.method | string | GET. |
saved_payment_method_id | string | Returned when this request saved a card with save_payment_method; omitted otherwise. Refresh Payment Methods to check availability. |
failure | object | Returned only for failed; omitted otherwise. |
failure.code | string | Provider or system failure code, when failure exists. See failure codes; codes are not translated into a separate enum. |
created_at | string | UTC payment creation time. |
updated_at | string | UTC time of the latest public payment change. |
expires_at | string | UTC deadline for customer action. Check status for the payment outcome. |
Card input and return destinations are never echoed. Fulfillment and refunds are on the order. A successful collection remains successful after a refund.
Response example — 201
json
{
"data": {
"payment_id": "pay_example",
"order_id": "ord_example",
"external_reference": "ORD-00042",
"customer_external_id": "CUS-1042",
"transaction_refids": [],
"status": "processing",
"currency": "MYR",
"payment_method_code": "CARD",
"customer_fee_percent": "50.00",
"fee": {
"type": "percentage",
"value": "1.70"
},
"totals": {
"items_subtotal": "100.00",
"fee": {
"total": "1.70",
"customer": "0.85",
"merchant": "0.85"
},
"payment_total": "100.85"
},
"action": {
"type": "redirect",
"url": "https://payments.example/checkout/session",
"method": "GET"
},
"created_at": "2026-09-14T02:05:00.000Z",
"updated_at": "2026-09-14T02:05:00.000Z",
"expires_at": "2026-09-14T02:35:00.000Z"
}
}HTTP 202 with status: "processing" means the outcome is not yet confirmed. Keep the payment ID and check its status; do not create a replacement.
Redirect and browser return
Open action.url when present so the customer can complete payment or authentication. Otherwise, follow the returned payment status.
For custom FPX, action.url redirects to the selected bank. Pine Labs binds the order/payment identifiers to your existing return_url and uses that URL for both success and failure returns. A browser return alone never settles a payment. Reusing an idempotency key with the same bank replays the admitted attempt; changing the bank with that key returns idempotency_conflict without another provider submission.
After checkout/authentication, the browser returns to this payment's return_url with order_id and payment_id. Those IDs are not authorization or proof of payment. Authenticate the customer and read current payment/order state.
Errors
| HTTP | Code | Action |
|---|---|---|
| 400 | validation_error | Fix the fields listed in error.details. Every invalid field is reported in one response. See validation details. |
| 400 | payment_method_unavailable | Refresh methods and choose an available one. |
| 400 | payment_method_details_required | Supply the method-specific details required for a new payment; if already supplied, contact support to check the configured field definitions. |
| 400 | invalid_payment_amount | FPX's calculated fee-inclusive total must be MYR 1.00–30,000.00. |
| 400 | invalid_customer | Supply a valid customer identity and the profile information required for this payment. |
| 400 | customer_profile_incomplete | Supply customer.name for this customer. |
| 400 | invalid_card_details | Correct the fields named in error.details; field values are never echoed. |
| 400 | invalid_payment_source | Do not send payment_method_details.card_number together with saved_payment_method_id. |
| 400 | provider_validation_error | The provider rejected this attempt's details. Keep the returned order/payment IDs. Reusing the same key returns the same rejection; use a new key only for an explicitly corrected attempt. |
| 404 | order_not_found | Check the order ID and access. |
| 404 | saved_payment_method_not_found | Refresh the saved methods for this customer and reseller account. |
| 409 | payment_already_successful | Read the existing order and the known payment. Do not charge again. |
| 409 | order_expired | The order no longer accepts new payments. Continue tracking any existing payment. |
| 409 | active_payment_exists | Wait for the existing attempt's outcome. Read it using the payment_id returned at creation; replay the same request/key first if that response was lost. |
| 409 | payment_creation_in_progress | Read GET /v2/payments/{payment_id} using error.payment_id; error.order_id identifies the order. Retry creation only with the same key/request; do not bypass it with a new charge. |
| 409 | idempotency_conflict | This key was already used with different request data. Check the existing payment before starting a new attempt. |
| 409 | total_mismatch | No payment created; confirm the fresh calculation below. |
| 503 | provider_customer_unavailable | The customer payment profile is temporarily unavailable; retry the same request later. |
A timeout or unresolved response is not a validation rejection. Retain the existing payment identity and follow its status rather than submitting a replacement charge.
See shared errors.
Total mismatch fields
error.details is the fresh calculation object for the order, including order_id and totals.
Show error.details.totals.payment_total and get customer confirmation before submitting the corrected request.
Cancel a payment
http
POST https://api.iimmpact.com/v2/payments/{payment_id}/cancelUse the same authentication headers and sign an empty body. No Idempotency-Key is required. Cancellation does not refund a successful payment.
CARD payments can be cancelled until they succeed. FPX payments cannot be cancelled once the bank page has been opened, because the customer can still complete payment there; wait for the final status instead. An unpaid FPX payment expires at its expires_at.
Path fields
| Field | Type | Required | Description |
|---|---|---|---|
payment_id | string | Yes | Your pending or processing CARD payment. |
Request example
bash
curl --request POST "https://api.iimmpact.com/v2/payments/$PAYMENT_ID/cancel" \
--header "X-Api-Key: $API_KEY" \
--header "X-Timestamp: $TIMESTAMP" \
--header "X-Nonce: $NONCE" \
--header "X-Signature: v1=$SIGNATURE"Response fields
Returns the same payment object inside data, with status: "cancelled" (200) or status: "cancellation_pending" (202).
Response example — 200
json
{
"data": {
"payment_id": "pay_example",
"order_id": "ord_example",
"external_reference": "ORD-00042",
"customer_external_id": "CUS-1042",
"transaction_refids": [],
"payment_method_code": "CARD",
"status": "cancelled",
"currency": "MYR",
"customer_fee_percent": "50.00",
"fee": { "type": "percentage", "value": "1.70" },
"totals": {
"items_subtotal": "100.00",
"fee": { "total": "1.70", "customer": "0.85", "merchant": "0.85" },
"payment_total": "100.85"
},
"created_at": "2026-09-14T02:05:00.000Z",
"updated_at": "2026-09-14T02:06:00.000Z",
"expires_at": "2026-09-14T02:35:00.000Z"
}
}Repeated calls return the existing cancellation result without resubmitting cancellation. Wait for a final outcome before retrying payment; collection can win a cancellation race.
Errors
| HTTP | Code | Action |
|---|---|---|
| 404 | payment_not_found | Check the payment ID; foreign payments also return 404. |
| 409 | payment_already_successful | The payment already succeeded and cannot be cancelled. error.order_id and error.payment_id identify it. |
| 409 | payment_not_cancellable | The payment already failed or expired, or it is an FPX payment already sent to the bank. Read the payment for its final status. error.order_id and error.payment_id identify it. |
Any other unresolved payment state returns 202 with cancellation_pending. See shared errors.
Collection lifecycle and conflicts
| Status | Meaning |
|---|---|
pending | Waiting for customer action; blocks another attempt. |
processing | Initiation or collection is unresolved; blocks another attempt. |
cancellation_pending | Cancellation is unresolved; blocks another attempt. |
successful | Collection verified. |
failed, cancelled, expired | Confirmed unsuccessful outcome; a new attempt may be eligible. |
See expiry and retry rules. Next: Track Results.
Refunds
Pine Labs handles refunds for failed fulfillment and duplicate collections. See Refunds for amounts and progress; refund data is returned on the order.

