Process ACH/eCheck payments via the Portico gateway using direct bank account entry — no card tokenization — demonstrated in PHP, Node.js, .NET, and Java.
-
SecCode.WEBis required on every eCheck object. All implementations set it (PHP:$check->secCode = SecCode::WEB, Node.js:check.secCode = SecCode.WEB, .NET:check.SecCode = "WEB", Java:check.setSecCode(SecCode.Web)). This tells Portico how the check was authorized. Omitting it causes a Portico rejection with an error message that does not namesecCode— the failure is hard to trace without knowing this is required. -
withAllowDuplicates(true)must be set when testing. Portico blocks repeat charges on the same account/routing combination. Without this flag, sandbox testing reliably hits duplicate-detection errors. All four implementations set it on the charge builder. -
No tokenization step — bank data travels directly from form to backend. There is no
globalpayments.jsiframe or client-side token. Each language has a matched per-language frontend (php/index.html,nodejs/index.html,dotnet/wwwroot/index.html,java/src/main/webapp/index.html). Node.js uses hyphenated field names (account-type,check-type); PHP, .NET, and Java use underscored names (account_type,check_type). Do not serve one language's frontend with another language's backend. -
PHP's
/configresponse differs from all other languages. Node.js, .NET, and Java return{"data":{"directEntry":true,"message":"..."}}. PHP'sconfig.phpreturns{"data":{"publicApiKey":"..."}}— it exposes the public key rather than thedirectEntryflag. The Node.js/.NET/Java shape is canonical for eCheck; the PHP shape is a copy-paste artifact from a card-payment template.
php/process-payment.php— eCheck processing entry point;configureSdk(),validateRoutingNumber(),sanitizeAccountNumber(),sanitizePostalCode()php/config.php— servesGET /config.php; returnspublicApiKey(diverges from other languages — see Critical Pattern 4)php/index.html— PHP-specific frontend; form posts directly toprocess-payment.phpwith underscored field namesphp/composer.json—globalpayments/php-sdk^13.1
nodejs/server.js—validateRoutingNumber,sanitizeAccountNumber,sanitizePostalCode; registersGET /configandPOST /process-paymentnodejs/index.html— Node.js-specific frontend; usesaccount-typeandcheck-type(hyphenated)nodejs/package.json—globalpayments-api^3.10.6
dotnet/Program.cs—ConfigureGlobalPaymentsSDK(),ConfigureEndpoints(),ValidateRoutingNumber(),SanitizeAccountNumber(),SanitizePostalCode()dotnet/wwwroot/index.html— .NET-specific frontenddotnet/dotnet.csproj—GlobalPayments.Api9.0.16
java/src/main/java/com/globalpayments/example/ProcessPaymentServlet.java—configureSDK(),validateRoutingNumber(),sanitizeAccountNumber(),sanitizePostalCode(); servlet handlesGET /configandPOST /ProcessPaymentServlet(mapped via@WebServlet(urlPatterns = {"/ProcessPaymentServlet", "/config"}))java/src/main/webapp/index.html— Java-specific frontendjava/pom.xml—globalpayments-sdk14.2.20
docker-compose.yml— multi-service config; includespythonandgoservice entries that reference non-existent directories — those services will fail to buildindex.html— root-level generic starter template; NOT the eCheck frontend — each language serves its own per-language copy
| Method | Path | Purpose |
|---|---|---|
| GET | /config |
Returns directEntry: true (Node.js/.NET/Java) or publicApiKey (PHP) |
| POST | /process-payment |
PHP / Node.js / .NET: validates ABA checksum; builds ECheck object; calls .charge().execute() via Portico |
| POST | /ProcessPaymentServlet |
Java only: same logic, different URL — servlet is mapped via @WebServlet(urlPatterns = {"/ProcessPaymentServlet", "/config"}) |
PHP does not use a router — GET /config is served by php/config.php and POST /process-payment by php/process-payment.php as separate files. Java's POST path is /ProcessPaymentServlet, not /process-payment.
PUBLIC_API_KEY=pkapi_cert_... # Portico public key — read by PHP config.php; not used in the ACH charge flow
SECRET_API_KEY=skapi_cert_... # Portico secret key — required for PorticoConfig SDK auth in all languages.env.sample files exist in each language directory. Copy to .env and fill in credentials before running.
This is an ACH/eCheck sample — no card numbers are involved.
| Field | Test Value |
|---|---|
| Account Number | 12345678901 |
| Routing Number | 122105155 (passes ABA checksum algorithm) |
| Account Type | checking |
| Check Type | personal |
| Amount | Any positive decimal (e.g. 25.00) |
Portico sandbox does not validate real account data — use any plausible format. Get credentials at developer.globalpayments.com.
Payment flow: Browser form → ABA checksum validation (client-side JS + server-side) → POST /process-payment (PHP/Node.js/.NET) or POST /ProcessPaymentServlet (Java) → ECheck.charge(amount).withAllowDuplicates(true).withCurrency("USD").execute() → Portico returns transactionId
No client-side tokenization: Unlike card-payment samples, no globalpayments.js is loaded. Bank account data goes directly from form fields to the backend over HTTPS.
These demos accept raw bank account numbers with no authentication on /process-payment. For production: add auth middleware, use tokenization to avoid handling raw account data server-side, and scope PCI DSS compliance accordingly.
cd php && ./run.sh # PHP — :8000 (Docker host: :8003)
cd nodejs && ./run.sh # Node.js — :8000 (Docker host: :8001)
cd dotnet && ./run.sh # .NET — :8000 (Docker host: :8006)
cd java && ./run.sh # Java — :8000 (Docker host: :8004)
# All at once:
docker-compose up# Config endpoint (Node.js, .NET, Java)
curl http://localhost:8000/config
# Expected: {"success":true,"data":{"directEntry":true,"message":"Direct bank account entry enabled"}}
# Process payment — PHP, .NET (underscored field names)
curl -X POST http://localhost:8000/process-payment \
-d "account_number=12345678901&routing_number=122105155&account_type=checking&check_type=personal&check_holder_name=Jane+Smith&amount=25.00"
# Expected: {"success":true,"message":"Payment successful! Transaction ID: ...","data":{"transactionId":"..."}}
# Process payment — Node.js (hyphenated field names)
curl -X POST http://localhost:8000/process-payment \
-d "account_number=12345678901&routing_number=122105155&account-type=checking&check-type=personal&check_holder_name=Jane+Smith&amount=25.00"
# Process payment — Java (different path: /ProcessPaymentServlet)
curl -X POST http://localhost:8000/ProcessPaymentServlet \
-d "account_number=12345678901&routing_number=122105155&account_type=checking&check_type=personal&check_holder_name=Jane+Smith&amount=25.00"All language implementations expose identical core behavior, with two structural exceptions: PHP implements endpoints as separate files (config.php, process-payment.php) rather than router registrations; Java uses /ProcessPaymentServlet as the POST path rather than /process-payment. A change to the charge logic must be applied to all four languages — each in a separate commit. Do not modify docker-compose.yml without noting that the python and go service blocks reference non-existent directories.
- PHP:
globalpayments/php-sdk^13.1 - Node.js:
globalpayments-api^3.10.6 - .NET:
GlobalPayments.Api9.0.16 - Java:
globalpayments-sdk14.2.20