Sign in

WaldenPay API

Accept card payments host-to-host: you collect card data on your own PCI DSS-compliant pages and submit it to the WaldenPay API. There is no hosted checkout page or redirect — your customer never leaves your site.

Overview

Base URL
https://gw.waldenpay.com
Format
JSON over HTTPS
Authentication
HTTP Basic (API endpoints), Bearer token (card charges)

A typical integration is three steps:

  1. Create a payment server-side and receive a one-time charge token.
  2. Charge the card using the token, with card data collected on your page.
  3. Confirm the outcome via webhook or by retrieving the payment.

Host-to-host mode requires PCI DSS compliance on your side, since card data passes through your pages and servers. Contact support to confirm your compliance status before going live.

How it works

The full payment lifecycle, from order to confirmation. Your customer never leaves your site except for an optional, issuer-hosted 3D Secure challenge.

Host-to-host payment sequence: create the payment, charge the card with optional 3DS, confirm the outcome
Host-to-host payment flow
  1. The customer places an order on your website.
  2. Your server creates a payment (POST /v1/payments) with your callback_url, return_url and the payer's customer profile, and receives a payment ID and a one-time charge token.
  3. You render your own card form. The customer enters their card details.
  4. Your server (or the payment page) submits the card data with the charge token (POST /v1/charges).
  5. If the issuer requires 3D Secure, the response contains an action — render it as an auto-submitting form; the customer completes the challenge on the issuer's page and returns to your return_url.
  6. WaldenPay records the final result and sends a signed payment.updated webhook.
  7. Your server confirms the outcome from the webhook — or by retrieving the payment — and shows the customer the result.

For transitional results (a charge response with status: "processing" and no action), don't poll in a tight loop — wait for the webhook, or reconcile after a short delay.

Authentication

All /v1 endpoints (except card charges) use HTTP Basic auth:

FieldValue
usernameYour merchant ID (wma_…)
passwordYour API key (wk_…)
curl https://gw.waldenpay.com/v1/payments \
  -u "wma_your_merchant_id:wk_your_api_key" \
  -H "Content-Type: application/json"

Card charges (POST /v1/charges) instead use Authorization: Bearer <charge token> — the single-use wgt_… token returned when the payment is created. Keep your API key server-side only; the charge token is safe to use from the page performing the charge.

Domains

Every payment must be associated with a domain you are approved to accept payments from. This domain is sent with the payment as its merchant_url, so a terminal cannot take payments until at least one domain is enabled for it.

How domains are set up:

  1. WaldenPay approves the set of domains allowed for your account. If you need a new domain added, contact support.
  2. In the Terminals page you choose which of your approved domains are enabled on each terminal. The Primary terminal always has every approved domain enabled.
  3. When you create a payment on a terminal, WaldenPay sends one of that terminal's enabled domains as the merchant_url.

In your API call, merchant_url is optional:

  • Omit it and WaldenPay uses the terminal's first enabled domain automatically — no code change is required if your terminal has domains enabled.
  • Send it to pick a specific domain when a terminal has several enabled. The value may be a bare host (shop.example.com) or a full URL (https://shop.example.com/checkout); it is normalized to the host. It must be one of that terminal's enabled domains.
Two failures to plan for: a payment on a terminal with no enabled domain is rejected with 422 no_enabled_domain, and a merchant_url that is not enabled for the terminal is rejected with 422 merchant_url_not_enabled. Enable the domain on the terminal (or have it approved for your account) before charging.

Create a payment

POST/v1/payments

Creates a payment and returns a charge token for submitting card data. Call this from your server when the customer starts checkout. The terminal must have at least one enabled domain — see Domains.

Request body

ParameterTypeDescription
amountnumberrequiredPayment amount, e.g. 42.5. Must be positive.
currencystringrequired3-letter ISO 4217 code — EUR or USD, depending on the processing currency enabled for your account region. Payments in a currency not enabled for your account fail with 422 unsupported_currency.
reference_idstringoptionalYour order reference. Must be unique per merchant; generated if omitted.
test_modebooleanoptionalCreate the payment in test mode. Defaults to false.
descriptionstringoptionalHuman-readable description.
merchant_urlstringoptionalDomain the payment originates from. Must be one of the domains enabled for the terminal; defaults to the terminal's first enabled domain. Payments fail with no_enabled_domain if the terminal has no enabled domain.
metadataobjectoptionalArbitrary key–value data returned with the payment.
customerobjectrequiredThe payer's details — see the field list below. Incomplete profiles fail with 422 invalid_customer.
callback_urlstringrequiredWebhook URL for this payment's status updates. May only be omitted if a default callback URL is configured for your account; otherwise creation fails with 422 missing_callback_url.
return_urlstringrequiredURL the customer returns to after 3DS. Missing or malformed values fail with 422 missing_return_url / 422 invalid_return_url.
return_urlsobjectoptionalPer-outcome return URLs.
expiresstringoptionalPayment expiry.

The customer object

Acquiring and anti-fraud rules require a complete payer profile with every payment. All fields below are validated at creation time; the first missing or malformed field is reported in the 422 invalid_customer error message.

FieldTypeDescription
first_namestringrequiredPayer's first name.
last_namestringrequiredPayer's last name.
emailstringrequiredPayer's email address.
phonestringrequiredPhone number in international format, e.g. "+14155550101".
date_of_birthstringrequiredPayer's date of birth, YYYY-MM-DD.
addressobjectrequiredPayer's billing address — see below.
reference_idstringoptionalYour identifier for this payer; used for recurring-payer recognition.
address fieldTypeDescription
addressstringrequiredStreet address line, e.g. "Main Str. 123".
citystringrequiredCity.
countrystringrequiredCountry name or code, e.g. "Germany".
postal_codestringrequiredPostal / ZIP code.
streetstringoptionalStreet name, if you track it separately from the address line.
statestringoptionalState / region code.

Example request

curl https://gw.waldenpay.com/v1/payments \
  -u "wma_your_merchant_id:wk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "reference_id": "order-1001",
    "amount": 42.5,
    "currency": "EUR",
    "test_mode": true,
    "merchant_url": "shop.example.com",
    "callback_url": "https://merchant.example/webhook",
    "return_url": "https://merchant.example/thanks",
    "description": "Order 1001",
    "customer": {
      "first_name": "Jane",
      "last_name": "Doe",
      "email": "[email protected]",
      "phone": "+14155550101",
      "date_of_birth": "1991-02-03",
      "address": {
        "address": "Main Str. 123",
        "city": "Frankfurt",
        "country": "Germany",
        "postal_code": "54321"
      }
    }
  }'

Response 201

{
  "payment": {
    "id": "wpi_k2m4x8p1q9r7s5t3",
    "reference_id": "order-1001",
    "status": "new",
    "is_final": false,
    "reason": null,
    "amount": 42.5,
    "currency": "EUR",
    "amount_paid": null,
    "amount_refunded": null,
    "test_mode": true,
    "description": "Order 1001",
    "metadata": null,
    "card": null,
    "charge": {
      "token": "wgt_7d2f9a1c4e6b8035",
      "url": "https://gw.waldenpay.com/v1/charges"
    },
    "callback_url": "https://merchant.example/webhook",
    "created": 1752566400,
    "updated": 1752566400
  }
}

Store payment.id and use charge.token in the next step. A duplicate reference_id returns 409 duplicate_reference_id.

Charge a card

POST/v1/charges

Submits card data for a payment. Authenticate with the charge token — no Basic auth:

Authorization: Bearer wgt_7d2f9a1c4e6b8035

Request body

ParameterTypeDescription
card_numberstringrequiredCard PAN, digits only.
cvvstringrequiredCard security code.
exp_monthstringrequiredTwo-digit expiry month, e.g. "10".
exp_yearstringrequiredTwo-digit expiry year, e.g. "35".
card_holderstringrequiredCardholder name as printed on the card.
browser_infoobjectrequiredThe payer's browser environment, used for 3DS 2.0 risk assessment — see the field list below. Incomplete objects fail with 422 invalid_browser_info.

browser_info fields

Acquiring and anti-fraud rules require the payer's real browser environment with every charge; accurate values improve frictionless 3DS approval rates. Collect them with JavaScript in the payer's browser (the expressions below) and pass them through unchanged. All fields are validated; the first missing or malformed field is reported in the 422 invalid_browser_info error message.

FieldTypeDescription
accept_headerstringrequiredThe Accept header the payer's browser sends for page loads, e.g. "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8".
color_depthnumberrequiredscreen.colorDepth, e.g. 24.
java_enabledbooleanrequirednavigator.javaEnabled(); false in modern browsers.
languagestringrequirednavigator.language, e.g. "en-US".
screen_heightnumberrequiredscreen.height in pixels.
screen_widthnumberrequiredscreen.width in pixels.
timezonestringrequiredIANA timezone name, Intl.DateTimeFormat().resolvedOptions().timeZone, e.g. "Europe/Berlin".
user_agentstringrequirednavigator.userAgent.
window_heightnumberrequiredwindow.innerHeight in pixels.
window_widthnumberrequiredwindow.innerWidth in pixels.

Example request

curl https://gw.waldenpay.com/v1/charges \
  -H "Authorization: Bearer wgt_7d2f9a1c4e6b8035" \
  -H "Content-Type: application/json" \
  -d '{
    "card_number": "4242424242424242",
    "card_holder": "JANE DOE",
    "cvv": "111",
    "exp_month": "12",
    "exp_year": "99",
    "browser_info": {
      "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
      "color_depth": 24,
      "java_enabled": false,
      "language": "en-US",
      "screen_width": 1920,
      "screen_height": 1080,
      "timezone": "Europe/Berlin",
      "user_agent": "Mozilla/5.0 ...",
      "window_width": 1920,
      "window_height": 1000
    }
  }'

Response 200

{
  "status": "processing",
  "is_final": false,
  "reason": null,
  "requires_action": true,
  "action": {
    "url": "https://gw.waldenpay.com/v1/continue/wpi_k2m4x8p1q9r7s5t3/challenge",
    "method": "POST",
    "params": { "creq": "eyJ0aHJlZURTU2VydmVy..." }
  }
}
OutcomeWhat to do
status: "succeeded"Payment complete — no further action.
requires_action: true3D Secure challenge required — see the next section.
status: "processing", no actionWait for the webhook or poll the payment.
status: "failed"Declined — reason holds the decline code.

3D Secure flow

When a charge returns requires_action: true, render the action as an auto-submitting form in the customer's browser: send action.params to action.url via action.method. Param names vary by acquirer — always iterate over whatever params contains.

<form id="challenge" action="{action.url}" method="{action.method}">
  <!-- one hidden input per entry in action.params -->
  <input type="hidden" name="creq" value="..." />
</form>
<script>document.getElementById("challenge").submit();</script>

After the customer completes the challenge they are returned to your return_url. The final result is delivered by webhook — always confirm the outcome server-side rather than trusting the browser redirect.

Retrieve a payment

GET/v1/payments/{id}
GET/v1/payments?reference_id={reference_id}

Fetches the live payment status. Use either the WaldenPay payment ID or your own reference_id. Both return { "payment": … } in the same shape as payment creation.

curl https://gw.waldenpay.com/v1/payments/wpi_k2m4x8p1q9r7s5t3 \
  -u "wma_your_merchant_id:wk_your_api_key"

Payment statuses

Only status determines the outcome. reason is a lower-case failure/decline code (e.g. insufficient_funds, invalid_cvv) or null.

StatusFinalMeaning
newCreated, awaiting card data.
processingCharge in progress.
authorizingAuthorization in progress.
authorizedAuthorized, awaiting capture.
refundingRefund in progress.
voidingVoid in progress.
succeededyesPaid in full.
partially_succeededyesPartially paid.
failedyesDeclined or failed — see reason.
expiredyesExpired before completion.
cancelledyesCancelled.
verifiedyesCard verified (no funds moved).
verification_failedyesCard verification failed.
refundedyesFully refunded.
partially_refundedyesPartially refunded.
charged_backyesCharged back by the cardholder.
partially_charged_backyesPartially charged back.
authorization_failedyesAuthorization declined.
voidedyesAuthorization voided.

Webhooks

On every status change WaldenPay POSTs to the payment's callback_url (falling back to your default callback URL):

{
  "event": "payment.updated",
  "payment": {
    "id": "wpi_k2m4x8p1q9r7s5t3",
    "reference_id": "order-1001",
    "status": "succeeded",
    "is_final": true,
    "amount": 42.5,
    "currency": "EUR"
  }
}

Verifying signatures

Every webhook carries an X-Signature header computed over the exact raw request body with your callback signing secret (ws_…):

X-Signature = base64( sha1( secret + rawBody + secret ) )
const crypto = require("node:crypto");

function verifySignature(secret, rawBody, header) {
  const expected = crypto
    .createHash("sha1")
    .update(secret + rawBody + secret)
    .digest("base64");
  return expected === header;
}

Delivery semantics

  • Respond 200 to acknowledge. Any other status is retried with a linearly increasing delay (1 minute, 2 minutes, 3 minutes...), up to 50 attempts.
  • Respond 429 to permanently stop delivery for that event.
  • Webhooks may arrive duplicated or out of order — deduplicate per delivery and order by payment.updated.

Testing

Pass "test_mode": true when creating a payment to run the whole flow against the test environment — test payments never move funds and never affect balances.

Never use real card numbers in test mode, and never send live orders through test payments. Use only the test cards below.

Test cards

Test cards come in batches matching the processing configuration of your account: the EUR batch for EUR accounts, and one of the two USD batches (A or B) for USD accounts — your account manager can confirm which one applies to each of your terminals. Cards from a batch that doesn't match your account are simply declined.

EUR batch

All EUR test cards share the same expiry and CVV: expiry 12/2099 (send exp_month: "12" and exp_year: "99" or "2099") and CVV 111. The card number selects the scenario:

Card number3DSResult
4111 1111 1111 1111nosucceeded
4242 4242 4242 4242yessucceeded after the challenge
2221 2221 2221 2219nofailed
2221 2221 2221 2227yesfailed after the challenge
5555 5555 5555 5565yesfailed — technical error during verification
3434 3434 3434 0000noprocessing for a while, then succeeded
3434 3434 3434 0091noprocessing for a while, then failed
4000 0000 0000 0010nofailed — operation not permitted
4000 0000 0000 0028nofailed — card limit exceeded
4000 0000 0000 0036nofailed — suspected fraud
4000 0000 0000 0044nofailed — card reported stolen

Any other card number (or a wrong CVV/expiry) is declined without 3DS. The four decline cards each return a distinct reason code with the failed status, so you can exercise your decline handling (including any Payment Retry logic).

USD batch A

Card numberCVVExpiry3DSResult
5123 8172 3406 0000anyany valid *yessucceeded
5519 2838 1203 0000anyany valid *nosucceeded
4412 3972 1208 0000anysee below **yesfailed
4302 9128 3702 0000anysee below **nofailed

* Use any valid expiry except 07/77. Expiry 07/77 leaves the payment in a transitional processing status on reconciliation — useful for testing your pending-payment handling.

** With the failing USD cards, the expiry date selects the decline reason returned with the failed status, so you can exercise each decline path (including your Payment Retry logic). Any expiry not listed below returns general_fatal_error.

USD batch A: decline reasons by expiry date

ExpiryreasonExpiryreason
08/56access_denied12/36functionality_is_not_permitted
07/55error11/35invalid_request
06/54provider_error10/34lost_or_stolen_card
05/53unknown09/33declined
04/52duplicated_transaction08/32invalid_otp
03/51auth_fatal_error07/31invalid_3ds_code
02/50unable_to_determine_3ds_enrolment06/30invalid_card_status
01/49card_is_3ds_enrolled05/29insufficient_funds
12/48card_is_not_3ds_enrolled04/28card_expired
11/47issuer_decline03/27invalid_cvv
10/46client_auth_failed02/26invalid_pan
09/45provider_fatal_error01/25invalid_credentials
08/44invalid_details06/42antifraud_error
07/43invalid_pin05/41invalid_card
04/40fatal_error02/38invalid_amount
03/39unable_to_reconcile01/37limit_violation

USD batch B

All USD batch B cards accept any CVV and any future expiry. Successful test deposits must stay below 10,000,000. The card number selects the scenario:

Card number3DSResult
4000 0000 0000 0408nosucceeded
5555 0000 0000 0107nosucceeded
4000 0000 0000 0002yessucceeded after the challenge
5555 0000 0000 0008yessucceeded after the challenge
4000 0000 0000 0416nofailed
5555 0000 0000 0115nofailed
4242 4242 4242 4242yesfailed after the challenge
5555 0000 0000 0438yesfailed after the challenge

What to test before going live

  • A successful 3DS payment (challenge form rendering and return flow).
  • A successful frictionless (non-3DS) payment.
  • Several declines — each decline card / expiry surfaces a different reason with status: "failed".
  • Webhook signature verification, deduplication and out-of-order delivery.
  • A payment that stays in processing before settling (EUR batch: 3434 3434 3434 0000; USD batch A: expiry 07/77) — poll or wait for the webhook.

Errors

All errors share one envelope:

{
  "error": {
    "code": "invalid_amount",
    "message": "'amount' must be a positive number"
  }
}
HTTPCodeDescription
401unauthorizedBad merchant credentials, missing/invalid charge token, or expired charge session.
404not_foundNo payment matches the given ID or reference.
409duplicate_reference_idA payment with this reference_id already exists.
409not_chargeableThe payment has no active charge session.
422invalid_amountamount is missing or not a positive number.
422invalid_currencycurrency is not a 3-letter ISO code.
422unsupported_currencycurrency is not enabled for processing on your account.
422missing_callback_urlNo callback_url on the payment and no default callback URL configured for the account.
422invalid_callback_urlcallback_url is not a valid http(s) URL.
422missing_return_urlreturn_url was not sent with the payment.
422invalid_return_urlreturn_url is not a valid http(s) URL.
422invalid_customerThe customer object is missing, or one of its required fields is missing or malformed. The message names the offending field.
422invalid_cardA required card field is missing or empty.
422invalid_browser_infoThe browser_info object is missing, or one of its required fields is missing or malformed. The message names the offending field.
422no_enabled_domainThe terminal has no enabled domain. Add one in the dashboard or contact support.
422merchant_url_not_enabledmerchant_url is not one of the terminal's enabled domains.
400missing_reference_idreference_id query parameter is required.
502payment_initiation_failedThe payment could not be initiated. Retry or contact support.
502charge_failedThe charge could not be processed. Retry.
502reconciliation_failedLive status is temporarily unavailable. Retry later.