Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Building a Payment Gateway API from Scratch

By Kokil Thapa | Last reviewed: September 2026

Building a Payment Gateway API from Scratch is not a weekend side project. It is a systems problem that spans money movement, fraud controls, reconciliation, and compliance boundaries most product teams underestimate until the first failed settlement run. Merchants do not want another CRUD app. They want an API that authorizes NPR or USD charges, survives duplicate POST requests, and tells their backend exactly when money actually moved. If you are evaluating whether to build or integrate, start with the contract merchants will code against — not your internal database schema. This guide walks through the architecture, endpoints, and production patterns I rely on when shipping REST API development work for eCommerce and client portals.

What is a payment gateway API and who should build one?

A payment gateway API sits between a merchant application and the financial rails that move money. The merchant sends a charge request. Your API validates it, routes it to an acquirer or local switch, stores immutable audit records, and returns a normalized response. Webhooks push asynchronous updates when authorization settles, fails, or disputes later.

Most teams should not build a full gateway. Stripe, PayPal, eSewa, Khalti, and ConnectIPS already solve card and wallet acceptance for Nepal and global merchants. You build from scratch when you operate a marketplace that must split payouts, white-label payments for resellers, or embed treasury logic a third party cannot expose. On production Laravel carts and legal-tech portals I have maintained, integration was the right default. Custom gateway work appeared only when settlement rules were the product.

Payment Gateway API ArchitectureMerchant AppShop / PortalGateway APIAuth + LedgerWebhooksAcquirerBank / SwitchInternal LedgerMySQL 9.7MerchantWebhook URL
High-level view of building a Payment Gateway API from Scratch — merchant requests flow through your API to acquirers, with ledger writes and webhook callbacks.

Before writing code, draw three boundaries. First, what card data touches your servers — that defines PCI scope. Second, which currency and settlement paths you support — NPR wallets differ from USD card capture. Third, how merchants authenticate — API keys for server-to-server, publishable keys for client-side tokenization only. Compare your needs against existing options in our eCommerce payment gateway comparison for Nepal before committing six months of engineering time.

Build versus integrate decision matrix

ScenarioIntegrate existing gatewayBuild gateway API
Single-store WooCommerce or Shopify checkoutYes — plugin or hosted fieldsNo — compliance cost exceeds benefit
Marketplace with split payouts to vendorsSometimes — Stripe Connect-style productsYes — if local rails lack split APIs
White-label payments for agency clientsRarely — branding limits hurtYes — API is the product
Nepal wallet + bank transfer onlyYes — eSewa, Khalti, ConnectIPSOnly if you are the wallet operator
Cross-border NPR and USD settlementOften — multi-currency acquirerYes — when FX rules are proprietary

Custom gateway work belongs in your roadmap when settlement logic is competitive advantage. For a standard checkout, read Laravel payment integrations and ship faster.

How do you design the core endpoints for a payment gateway API?

Design the public contract first. Merchants think in charges, refunds, customers, and payment methods — not your internal transaction table. Follow REST nouns, version the base path, and never break v1 without a sunset plan described in Laravel API versioning strategy.

A minimal v1 surface looks like this:

  • POST /v1/charges — create an authorization or sale
  • GET /v1/charges/{id} — retrieve status and receipt fields
  • POST /v1/charges/{id}/capture — capture a prior authorization
  • POST /v1/refunds — partial or full refund against a charge
  • POST /v1/payment_methods — attach tokenized instrument to customer
  • GET /v1/balance_transactions — settlement lines for reconciliation

Return consistent JSON envelopes. Use HTTP status codes correctly: 201 for creates, 402 for declined payments, 409 for idempotency conflicts, 422 for validation errors. Document every field in OpenAPI and publish it — merchants treat undocumented keys as bugs.

Charge Request LifecycleMerchant POSTIdempotency-KeyPOST/v1/chargesValidate + Ledgerpending stateAcquirer Callauthorize or saleUpdate Statussucceeded / failedWebhook POSTcharge.succeeded
Typical charge flow when building a Payment Gateway API from Scratch — idempotent POST, ledger write, acquirer authorization, then signed webhook.

Example charge request and response

Merchants send amounts in the smallest currency unit. Rs 1,500.00 NPR becomes 150000 paisa if you follow minor-unit conventions, or 1500 if you document whole rupees — pick one and never mix them. Use the same approach as our Nepal EMI calculator docs: explicit currency metadata on every object.

POST /v1/charges HTTP/1.1
Host: api.example.com
Authorization: Bearer sk_live_...
Idempotency-Key: ord_9f3a-20260908-001
Content-Type: application/json

{
  "amount": 150000,
  "currency": "npr",
  "payment_method": "pm_tok_abc123",
  "capture": true,
  "metadata": {
    "order_id": "ORD-8842",
    "merchant_ref": "shop-checkout-44"
  }
}
{
  "id": "ch_7Km2pL9x",
  "object": "charge",
  "amount": 150000,
  "currency": "npr",
  "status": "succeeded",
  "captured": true,
  "failure_code": null,
  "created": 1757315520,
  "metadata": {
    "order_id": "ORD-8842"
  }
}

Validate payloads with Form Requests in Laravel 13 on PHP 8.3 or higher. Persist the raw request body before calling external systems. If the acquirer times out, you need the original payload to reconcile — not a reconstructed guess from logs.

Laravel route and controller sketch

// routes/api.php
Route::prefix('v1')->middleware(['auth:sanctum', 'throttle:payments'])->group(function () {
    Route::post('/charges', [ChargeController::class, 'store']);
    Route::get('/charges/{charge}', [ChargeController::class, 'show']);
    Route::post('/charges/{charge}/capture', [ChargeController::class, 'capture']);
    Route::post('/refunds', [RefundController::class, 'store']);
});

Keep controllers thin. A CreateCharge action should orchestrate: idempotency lookup, ledger insert, acquirer adapter call, state transition, and domain event dispatch for webhook workers. Patterns from building RESTful APIs with Laravel apply directly — payments just add stricter invariants.

How do you implement idempotency and webhooks reliably?

Duplicate POST requests are guaranteed in production. Mobile clients retry. Load balancers replay. Merchants double-click. Without idempotency, you double-charge — the fastest way to destroy trust.

Require an Idempotency-Key header on all POST endpoints that move money. Store a hash of the merchant ID, key, route, and request body. Return the original response for repeats within 24 hours. Return 409 if the same key arrives with a different body. Redis 8.10 handles hot lookups; MySQL holds the authoritative record. Read the dedicated guide on API idempotency keys for TTL and cleanup jobs.

  1. Merchant sends POST with idempotency key.
  2. Gateway checks idempotency_keys table inside a database transaction.
  3. If new, insert ledger row as pending and call acquirer.
  4. On response, update charge status and cache serialized HTTP response.
  5. Duplicate request returns cached response without second acquirer call.

Webhook design merchants can trust

Webhooks are your real-time contract. Merchants should never poll GET /charges/{id} every second during checkout. Emit events: charge.succeeded, charge.failed, refund.created, dispute.opened.

Sign every payload with HMAC-SHA256 using a per-merchant secret. Include a timestamp header and reject events older than five minutes to block replay attacks. Retry with exponential backoff across at least 72 hours. Log delivery attempts in a webhook_deliveries table with response codes.

X-Gateway-Signature: t=1757315520,v1=8f4e2b...
X-Gateway-Event: charge.succeeded

{
  "id": "evt_3Nx8q",
  "type": "charge.succeeded",
  "data": { "object": { "id": "ch_7Km2pL9x", "status": "succeeded" } }
}

Merchants verify signatures before updating order status. On a real client project, the bug I see most often is updating the cart on HTTP 200 from the charge endpoint instead of waiting for the webhook — treat synchronous responses as provisional until settlement confirms.

Webhook Delivery and RetriesEvent QueueRedis jobHTTP POSTsigned payloadMerchant200 OKverify sigRetry Worker1m 5m 30m 2hDead Lettermanual replay4xx/5xx/timeoutmax retries
Webhook retry pipeline for a payment gateway API — signed delivery, exponential backoff, and dead-letter queue for ops replay.

Publish a merchant SDK or copy-paste verifier. Good SDK design reduces support tickets — see SDK design for your public API. Provide a CLI to replay dead-letter events after the merchant fixes their endpoint.

What security and PCI scope rules apply to a custom gateway?

Security failures end companies. You do not store raw PANs unless you pursue PCI DSS Level 1 certification — audit costs alone can exceed Rs 40 lakh (~USD 3,000) annually before infrastructure hardening. Default architecture: hosted fields or JS tokenization so card numbers never touch your application servers.

Follow the PCI Security Standards Council document library for SAQ types. Most custom gateways target SAQ A or SAQ A-EP by isolating card data in a PCI-compliant vault provider. Your API stores only tokens like pm_tok_abc123.

  • Transport: TLS 1.2+ everywhere, HSTS on all API hosts.
  • Authentication: Separate publishable and secret keys; rotate secrets without downtime.
  • Authorization: Scope keys — refund-capable keys are not the same as charge-only keys.
  • Rate limiting: Per-key and per-IP throttles on charge endpoints.
  • Audit: Append-only ledger; never delete charge rows — soft-delete with tombstone flags.
  • PII: Encrypt sensitive metadata at rest; mask card last-four in logs.

Authentication patterns mirror general API work covered in building secure authentication systems and Passport vs Sanctum for API auth. Use Sanctum for first-party merchant dashboards and hashed API keys for server integrations.

PCI Scope Reduction via TokenizationCheckout JSbrowserToken VaultPAN stays herePCI zoneGateway APItokens onlyAcquirerauthorizationLedger DBno raw PANcardpm_tok
Reduce PCI scope when building a Payment Gateway API from Scratch — card data enters a token vault; your API handles tokens and ledger entries only.

For Nepal bank transfers via ConnectIPS, the pattern differs — users redirect to bank apps and callbacks carry signed transaction references. Study ConnectIPS integration for bank payments before abstracting a unified charge endpoint across cards and bank rails.

How do you test, reconcile, and deploy a payment gateway API?

Payments require test modes that mirror production exactly. Issue sk_test_ and sk_live_ key prefixes. Simulate acquirer outcomes with deterministic test PANs: one ends in 4242 for success, another triggers insufficient funds, another triggers timeout.

Reconciliation is where amateur gateways die. Nightly batch jobs compare your ledger against acquirer settlement files. Mismatches flag ops review — never auto-delete. On eCommerce projects like Quick And Easy Nepalese Grocery, reconciliation reports saved hours when a gateway CSV did not match order totals after a partial refund bug.

Deployment checklist

  1. Run migrations on MySQL 9.7 with strict mode enabled.
  2. Configure queue workers for webhooks on a dedicated Redis queue.
  3. Set APP_KEY rotation procedure without invalidating stored secrets.
  4. Enable slow-query logging on ledger tables before launch.
  5. Ship OpenAPI docs via Scribe — see API documentation with Scribe.
  6. Run contract tests on charge and refund JSON schemas using JSON formatter fixtures in CI.
  7. Load-test idempotency under concurrent duplicate POSTs.

Deploy with zero-downtime releases on Ubuntu 24 and PHP-FPM 8.5. Reload FPM after symlink swap so opcache picks up new code. Use GitLab CI to lint, run PHPUnit, and deploy through Deployer 7 — the same pipeline pattern I use on sister legal-tech sites. External reference: the Stripe API reference is the best public example of consistent idempotency, pagination, and error objects — study it even if Stripe is your competitor.

Magento and WooCommerce merchants expect plugin interfaces. If you white-label, plan adapter modules early — read Magento 2 custom payment gateway integration and WooCommerce custom payment gateway development for checkout expectations.

Operational monitoring belongs in testing and optimization retainers: alert on charge latency p95, webhook failure rate, and reconciliation drift. A gateway that silently stops settling is worse than one that throws loud 500 errors.

Key Takeaways

  • Building a Payment Gateway API from Scratch is justified only when settlement or white-label logic is core product — otherwise integrate.
  • Version your REST contract, require idempotency keys on every money-moving POST, and cache duplicate responses.
  • Never store raw card numbers on application servers; tokenize through a PCI-scoped vault.
  • Deliver signed webhooks with retries and dead-letter replay — merchants should confirm orders on events, not synchronous charge responses.
  • Run nightly reconciliation between your ledger and acquirer files before you trust dashboard totals.
  • Document with OpenAPI, ship test keys and deterministic fixtures, and load-test concurrent duplicate charges.

People Also Ask

How long does it take to build a payment gateway API?

A minimal charge-and-refund API with webhooks takes three to six months for a senior team. PCI certification, multi-acquirer routing, dispute handling, and merchant dashboards add another six to twelve months. Integration with existing gateways often ships in two to four weeks for Laravel or WooCommerce stores.

Do I need a banking license to run a payment gateway?

Regulation depends on jurisdiction. In Nepal, payment service operations fall under Nepal Rastra Bank oversight for licensed payment service providers. Most software agencies build tech for licensed partners rather than becoming the licensed entity. Legal review is mandatory before handling customer funds.

What database schema do payment gateways use?

At minimum: merchants, api_keys, customers, payment_methods (tokens only), charges, refunds, idempotency_keys, ledger_entries, webhook_endpoints, and webhook_deliveries. Treat charges and ledger_entries as append-heavy tables with indexed status and created_at columns for reporting.

Can Laravel handle a production payment gateway API?

Yes. Laravel 13 on PHP 8.3+ provides queues, Sanctum, Form Requests, and Eloquent transactions suited to gateway workloads. Pair it with Redis 8.10 for idempotency caches and webhook jobs. High-volume card routing may later split acquirer adapters into dedicated services, but Laravel remains a solid control plane.

Ship the gateway — or integrate wisely

Building a Payment Gateway API from Scratch demands ledger discipline, idempotent endpoints, and PCI-aware tokenization before you write a single acquirer adapter. Most businesses should integrate proven rails and spend engineering budget on checkout conversion and reconciliation tooling instead. If your product genuinely needs custom settlement, start with a versioned charge contract and webhook spec — then iterate toward certification and bank partnerships. For architecture review or eCommerce development with Nepal payment rails, contact us or explore custom software development options on kokil.com.np.

Frequently Asked Questions

A payment gateway API sits between a merchant application and the financial rails that move money. The merchant sends a charge request; your API validates it, routes it to an acquirer or local switch, stores immutable audit records, and returns a normalized response. Webhooks push asynchronous updates when authorization settles, fails, or disputes later.

Most teams should not build a full gateway. Stripe, PayPal, eSewa, Khalti, and ConnectIPS already solve card and wallet acceptance for Nepal and global merchants. Build from scratch when you operate a marketplace that must split payouts, white-label payments for resellers, or embed treasury logic a third party cannot expose. On production Laravel carts and legal-tech portals I have maintained, integration was the right default. Custom gateway work appeared only when settlement rules were the product.

A minimal charge-and-refund API with webhooks takes three to six months for a senior team. PCI certification, multi-acquirer routing, dispute handling, and merchant dashboards add another six to twelve months. Integrating an existing gateway often ships in two to four weeks for Laravel or WooCommerce stores.

Regulation depends on jurisdiction. In Nepal, payment service operations fall under Nepal Rastra Bank oversight for licensed payment service providers. Most software agencies build tech for licensed partners rather than becoming the licensed entity. Legal review is mandatory before handling customer funds.

Design the public contract first. Merchants think in charges, refunds, customers, and payment methods, not your internal transaction table. A minimal v1 surface includes POST /v1/charges for authorization or sale, GET /v1/charges/{id} for status, POST /v1/charges/{id}/capture for prior authorizations, POST /v1/refunds for partial or full refunds, POST /v1/payment_methods for tokenized instruments, and GET /v1/balance_transactions for reconciliation lines. Version the base path and document every field in OpenAPI.

Return consistent JSON envelopes and use HTTP status codes correctly throughout the contract. Use 201 for successful creates, 402 for declined payments, 409 for idempotency conflicts when the same key arrives with a different body, and 422 for validation errors. Merchants treat undocumented response keys as bugs, so document every field and keep status semantics stable across v1.

Duplicate POST requests are guaranteed in production from mobile retries, load balancer replays, and merchant double-clicks. Require an Idempotency-Key header on all POST endpoints that move money. Store a hash of the merchant ID, key, route, and request body. Return the original response for repeats within 24 hours and 409 if the same key arrives with a different body. Redis 8.10 handles hot lookups; MySQL holds the authoritative record inside a database transaction before any acquirer call.

Webhooks are your real-time contract; merchants should not poll GET /charges/{id} every second during checkout. Emit events like charge.succeeded, charge.failed, refund.created, and dispute.opened. Sign every payload with HMAC-SHA256 using a per-merchant secret, include a timestamp header, and reject events older than five minutes to block replay attacks. Retry with exponential backoff across at least 72 hours and log delivery attempts in a webhook_deliveries table. Provide a CLI to replay dead-letter events after the merchant fixes their endpoint.

Treat synchronous charge responses as provisional until settlement confirms. On real client projects, the bug I see most often is updating the cart on HTTP 200 from the charge endpoint instead of waiting for the webhook. Merchants should verify webhook signatures before updating order status. Good SDK design with a copy-paste verifier reduces support tickets when teams implement this pattern correctly.

Security failures end companies. You do not store raw PANs unless you pursue PCI DSS Level 1 certification; audit costs alone can exceed Rs 40 lakh (~USD 3,000) annually before infrastructure hardening. Default architecture uses hosted fields or JS tokenization so card numbers never touch your application servers. Most custom gateways target SAQ A or SAQ A-EP by isolating card data in a PCI-compliant vault provider. Your API stores only tokens like pm_tok_abc123. Also enforce TLS 1.2+, separate publishable and secret keys, scoped authorization, rate limiting, and append-only ledger audit trails.

Merchants send amounts in the smallest currency unit. Rs 1,500.00 NPR becomes 150000 paisa if you follow minor-unit conventions, or 1500 if you document whole rupees. Pick one approach and never mix them. Include explicit currency metadata on every object, the same pattern used in explicit currency documentation elsewhere. Validate payloads with Form Requests in Laravel 13 on PHP 8.3 or higher, and persist the raw request body before calling external systems so you can reconcile after acquirer timeouts.

At minimum your schema needs merchants, api_keys, customers, payment_methods storing tokens only, charges, refunds, an append-only ledger recording every state transition, idempotency_keys for duplicate POST handling, and webhook_deliveries for retry logging. Never delete charge rows; soft-delete with tombstone flags instead. Run migrations on MySQL 9.7 with strict mode enabled and enable slow-query logging on ledger tables before launch, because reconciliation queries against these tables become your source of truth.

Payments require test modes that mirror production exactly. Issue sk_test_ and sk_live_ key prefixes. Simulate acquirer outcomes with deterministic test PANs: one ending in 4242 for success, another for insufficient funds, another for timeout. Run contract tests on charge and refund JSON schemas using JSON formatter fixtures in CI. Load-test idempotency under concurrent duplicate POSTs. Ship OpenAPI docs via Scribe and study the Stripe API reference for consistent idempotency, pagination, and error object patterns even if Stripe is your competitor.

Reconciliation is where amateur gateways die. Nightly batch jobs compare your ledger against acquirer settlement files. Mismatches flag ops review; never auto-delete discrepancies. On eCommerce projects like Quick And Easy Nepalese Grocery, reconciliation reports saved hours when a gateway CSV did not match order totals after a partial refund bug. Operational monitoring should alert on charge latency p95, webhook failure rate, and reconciliation drift. A gateway that silently stops settling is worse than one that throws loud 500 errors.

Deploy with zero-downtime releases on Ubuntu 24 and PHP-FPM 8.5. Reload FPM after symlink swap so opcache picks up new code. Configure queue workers for webhooks on a dedicated Redis queue. Use GitLab CI to lint, run PHPUnit, and deploy through Deployer 7, the same pipeline pattern I use on sister legal-tech sites. Set an APP_KEY rotation procedure without invalidating stored merchant secrets. Use Sanctum for first-party merchant dashboards and hashed API keys for server integrations, with per-key and per-IP throttles on charge endpoints.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: