286 lines
13 KiB
JSON
286 lines
13 KiB
JSON
{
|
|
"openapi": "3.0.3",
|
|
"info": {
|
|
"title": "Midtrans Middleware API",
|
|
"version": "1.0.0",
|
|
"description": "Express backend integrating Midtrans (Core API + Snap) with an ERP system. Supports two payment-creation flows: a pre-registered shareable payment link (`/createtransaksi`, `POST /api/payment-links`) for ERP-initiated orders, and a direct checkout flow (`/api/payments/charge`, `/api/payments/snap/token`) called straight from the browser."
|
|
},
|
|
"servers": [
|
|
{ "url": "http://localhost:8000", "description": "Local" },
|
|
{ "url": "https://be-midtrans-cifo.winteraccess.id", "description": "Production" }
|
|
],
|
|
"components": {
|
|
"securitySchemes": {
|
|
"ApiKeyAuth": {
|
|
"type": "apiKey",
|
|
"in": "header",
|
|
"name": "X-API-KEY",
|
|
"description": "Required in production (EXTERNAL_API_KEY). Bypassed automatically when NODE_ENV is not 'production' and the key is unset."
|
|
},
|
|
"BasicAuth": {
|
|
"type": "http",
|
|
"scheme": "basic",
|
|
"description": "Required in production (LOG_BASIC_AUTH_USER/PASS). Bypassed automatically when NODE_ENV is not 'production' and the credentials are unset."
|
|
}
|
|
},
|
|
"schemas": {
|
|
"Customer": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": { "type": "string" },
|
|
"phone": { "type": "string" },
|
|
"email": { "type": "string" }
|
|
}
|
|
},
|
|
"ErrorResponse": {
|
|
"type": "object",
|
|
"properties": {
|
|
"error": { "type": "string" },
|
|
"message": { "type": "string" }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"paths": {
|
|
"/api/health": {
|
|
"get": {
|
|
"summary": "Health check",
|
|
"tags": ["Health & Config"],
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK",
|
|
"content": { "application/json": { "example": { "ok": true, "env": { "isProduction": true, "hasServerKey": true, "hasClientKey": true } } } }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"/api/config": {
|
|
"get": {
|
|
"summary": "Get runtime payment-method toggles and Midtrans client key",
|
|
"tags": ["Health & Config"],
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK",
|
|
"content": { "application/json": { "example": { "paymentToggles": { "bank_transfer": true, "credit_card": true, "gopay": true, "cstore": true }, "midtransEnv": "production", "clientKey": "Mid-client-xxx" } } }
|
|
}
|
|
}
|
|
},
|
|
"post": {
|
|
"summary": "Update payment-method toggles at runtime (non-production only)",
|
|
"tags": ["Health & Config"],
|
|
"requestBody": {
|
|
"content": { "application/json": { "example": { "paymentToggles": { "bank_transfer": false } } } }
|
|
},
|
|
"responses": {
|
|
"200": { "description": "Updated" },
|
|
"403": { "description": "Disabled in production", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
|
|
}
|
|
}
|
|
},
|
|
"/api/payment-links": {
|
|
"post": {
|
|
"summary": "Create a shareable Midtrans Snap payment link from a direct payload",
|
|
"description": "order_id is used as-is for tracking, and sanitized (any character outside [A-Za-z0-9-_~.] -> '.') for the value actually sent to Midtrans. mercant_id used for the ERP callback is derived from the segment before ':' in the original order_id. Rate-limited.",
|
|
"tags": ["Payment Links"],
|
|
"security": [{ "ApiKeyAuth": [] }],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"example": {
|
|
"order_id": "ERPSKRIP-2608030000000627:TKG-260803000063",
|
|
"nominal": 179000,
|
|
"customer": { "name": "Yusnika Nur Faidah", "phone": "0881022144656", "email": "yusnika_nur_faidah@example.com" },
|
|
"expire_at": 1785852063058
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Link created",
|
|
"content": { "application/json": { "example": { "status": "200", "messages": "SUCCESS", "data": { "url": "http://localhost:5173/pay/eyJ2Ijox...", "order_id": "ERPSKRIP-2608030000000627:TKG-260803000063", "midtrans_order_id": "ERPSKRIP-2608030000000627.TKG-260803000063", "expire_at": 1785852063058 } } } }
|
|
},
|
|
"400": { "description": "Missing/invalid order_id or nominal" },
|
|
"401": { "description": "Invalid X-API-KEY" },
|
|
"409": { "description": "Order already completed or has an active link/pending Midtrans transaction" },
|
|
"429": { "description": "Rate limited" }
|
|
}
|
|
}
|
|
},
|
|
"/api/payment-links/{token}": {
|
|
"get": {
|
|
"summary": "Resolve a payment link token",
|
|
"tags": ["Payment Links"],
|
|
"parameters": [{ "name": "token", "in": "path", "required": true, "schema": { "type": "string" } }],
|
|
"responses": {
|
|
"200": {
|
|
"description": "OK",
|
|
"content": { "application/json": { "example": { "order_id": "ERPSKRIP-2608030000000627.TKG-260803000063", "nominal": 179000, "customer": { "name": "Yusnika Nur Faidah", "phone": "0881022144656", "email": "yusnika_nur_faidah@example.com" }, "expire_at": 1785852063058, "allowed_methods": null } } }
|
|
},
|
|
"400": { "description": "Invalid token" },
|
|
"410": { "description": "Token expired" }
|
|
}
|
|
}
|
|
},
|
|
"/createtransaksi": {
|
|
"post": {
|
|
"summary": "Create a shareable Midtrans Snap payment link (ERP-facing, mercant_id/item shape)",
|
|
"description": "order_id is derived server-side as `mercant_id.item[0].item_id` (dot-joined). expire_at is NOT client-supplied — computed server-side from PAYMENT_LINK_TTL_MINUTES. Rate-limited.",
|
|
"tags": ["Payment Links"],
|
|
"security": [{ "ApiKeyAuth": [] }],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"example": {
|
|
"mercant_id": "ERPSKRIP-2608030000000627",
|
|
"nominal": 179000,
|
|
"nama": "Yusnika Nur Faidah",
|
|
"no_telepon": "0881022144656",
|
|
"email": "yusnika_nur_faidah@example.com",
|
|
"item": [{ "item_id": "TKG-260803000063" }],
|
|
"allowed_methods": ["bank_transfer", "gopay"]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": { "description": "Link created", "content": { "application/json": { "example": { "status": "200", "messages": "SUCCESS", "data": { "url": "http://localhost:5173/pay/eyJ2Ijox..." } } } } },
|
|
"400": { "description": "Missing order_id/nominal" },
|
|
"401": { "description": "Invalid X-API-KEY" },
|
|
"409": { "description": "Order already completed or has an active link/pending Midtrans transaction" },
|
|
"429": { "description": "Rate limited" }
|
|
}
|
|
}
|
|
},
|
|
"/api/payments/charge": {
|
|
"post": {
|
|
"summary": "Create a payment transaction via Midtrans Core API (bank transfer, card, GoPay/QRIS, cstore)",
|
|
"description": "Called directly from the browser checkout (no pre-registration required). Rate-limited to mitigate abuse/card-testing. Blocks re-charge if the order already has a pending Midtrans transaction, and honors ENABLE_* payment-method toggles.",
|
|
"tags": ["Payment Operations"],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"example": { "payment_type": "bank_transfer", "transaction_details": { "order_id": "order-123", "gross_amount": 150000 }, "bank_transfer": { "bank": "bca" } }
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": { "description": "Raw Midtrans Core API charge response (passthrough)" },
|
|
"400": { "description": "Charge failed / payment type disabled" },
|
|
"409": { "description": "Order already has a pending Midtrans transaction" },
|
|
"429": { "description": "Rate limited" }
|
|
}
|
|
}
|
|
},
|
|
"/api/payments/snap/token": {
|
|
"post": {
|
|
"summary": "Generate a Midtrans Snap token for the hosted payment popup",
|
|
"description": "Called directly from the browser (PayPage / CheckoutPage) with a raw Midtrans Snap createTransaction payload. Rate-limited.",
|
|
"tags": ["Payment Operations"],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"example": { "transaction_details": { "order_id": "order-123", "gross_amount": 150000 }, "customer_details": { "first_name": "John", "email": "john@example.com", "phone": "081234567890" }, "item_details": [{ "id": "order-123", "name": "Payment", "price": 150000, "quantity": 1 }] }
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": { "description": "Snap token", "content": { "application/json": { "example": { "token": { "token": "snap-token-xxx", "redirect_url": "https://app.midtrans.com/snap/v#/xxx" } } } } },
|
|
"400": { "description": "Snap token creation failed" },
|
|
"429": { "description": "Rate limited" }
|
|
}
|
|
}
|
|
},
|
|
"/api/payments/{orderId}/status": {
|
|
"get": {
|
|
"summary": "Check Midtrans transaction status",
|
|
"description": "Passthrough of Midtrans core.transaction.status(). Also fires the ERP-notify fallback (fire-and-forget) if the status is already successful and hasn't been notified yet.",
|
|
"tags": ["Payment Operations"],
|
|
"parameters": [{ "name": "orderId", "in": "path", "required": true, "schema": { "type": "string" } }],
|
|
"responses": {
|
|
"200": { "description": "Raw Midtrans status response (passthrough)" }
|
|
}
|
|
}
|
|
},
|
|
"/api/payments/notification": {
|
|
"post": {
|
|
"summary": "Midtrans webhook (unified for Core and Snap)",
|
|
"description": "Verifies signature_key (SHA512(order_id+status_code+gross_amount+MIDTRANS_SERVER_KEY)), maps status, and notifies the ERP with { mercant_id, status_code, nominal, signature }. Returns non-2xx on ERP-notify failure so Midtrans retries.",
|
|
"tags": ["Webhook"],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"example": { "order_id": "ERPSKRIP-2608030000000637.TKG-260801001361", "transaction_status": "settlement", "status_code": "200", "gross_amount": "159000.00", "signature_key": "..." }
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": { "description": "Processed (or already processed)" },
|
|
"400": { "description": "Invalid signature" },
|
|
"500": { "description": "ERP notification failed — Midtrans will retry" }
|
|
}
|
|
}
|
|
},
|
|
"/api/logs": {
|
|
"get": {
|
|
"summary": "Get recent in-memory log entries",
|
|
"tags": ["Logs (internal)"],
|
|
"security": [{ "BasicAuth": [] }],
|
|
"parameters": [
|
|
{ "name": "limit", "in": "query", "schema": { "type": "integer", "default": 100, "minimum": 1, "maximum": 1000 } },
|
|
{ "name": "level", "in": "query", "schema": { "type": "string", "enum": ["debug", "info", "warn", "error"] } },
|
|
{ "name": "q", "in": "query", "schema": { "type": "string" }, "description": "Keyword search" }
|
|
],
|
|
"responses": {
|
|
"200": { "description": "OK" },
|
|
"401": { "description": "Basic Auth required" },
|
|
"403": { "description": "LOG_EXPOSE_API disabled" }
|
|
}
|
|
}
|
|
},
|
|
"/api/logs/files": {
|
|
"get": {
|
|
"summary": "List available daily log files",
|
|
"tags": ["Logs (internal)"],
|
|
"security": [{ "BasicAuth": [] }],
|
|
"responses": {
|
|
"200": { "description": "OK", "content": { "application/json": { "example": { "count": 2, "files": [{ "filename": "LOGS_03082026.log", "size": 39013, "modified": "2026-08-03T22:30:00.000Z", "path": "/api/logs/files/LOGS_03082026.log" }] } } } },
|
|
"401": { "description": "Basic Auth required" }
|
|
}
|
|
}
|
|
},
|
|
"/api/logs/files/{filename}": {
|
|
"get": {
|
|
"summary": "Read a specific log file as JSON lines (or redirects to the HTML viewer for browser navigation)",
|
|
"description": "Browser requests (Accept: text/html) are redirected to GET /api/logs/view?file=... instead of returning raw JSON.",
|
|
"tags": ["Logs (internal)"],
|
|
"security": [{ "BasicAuth": [] }],
|
|
"parameters": [{ "name": "filename", "in": "path", "required": true, "schema": { "type": "string", "pattern": "^LOGS_\\d{8}\\.log$" } }],
|
|
"responses": {
|
|
"200": { "description": "OK", "content": { "application/json": { "example": { "filename": "LOGS_03082026.log", "lines": 5777, "content": ["[2026-08-03T21:35:54.746+07:00] [INFO ] webhook.notifying_erp | {\"order_id\":\"...\"}"] } } } },
|
|
"302": { "description": "Redirect to the HTML viewer (browser navigation)" },
|
|
"400": { "description": "Invalid filename" },
|
|
"401": { "description": "Basic Auth required" },
|
|
"404": { "description": "File not found" }
|
|
}
|
|
}
|
|
},
|
|
"/api/logs/view": {
|
|
"get": {
|
|
"summary": "Readable log viewer — search, level filter, click-to-trace",
|
|
"tags": ["Logs (internal)"],
|
|
"security": [{ "BasicAuth": [] }],
|
|
"parameters": [{ "name": "file", "in": "query", "schema": { "type": "string" }, "description": "Defaults to the most recent log file" }],
|
|
"responses": {
|
|
"200": { "description": "HTML page", "content": { "text/html": {} } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|