{"openapi": "3.1.0", "info": {"title": "Samsoftpay API", "version": "1.0.0", "summary": "Mobile-Money payment gateway for Uganda.", "description": "MTN Mobile Money collections & disbursements, hosted checkout, payment links, vending payments, subaccount splits and signed webhooks. One base URL for test and live \u2014 the key prefix (`sk_test_` vs `sk_live_`) selects the mode. Human docs: https://samsoftpay.com/docs \u00b7 Markdown: https://samsoftpay.com/docs.md", "contact": {"name": "Samsoftpay", "url": "https://samsoftpay.com/docs"}}, "servers": [{"url": "https://samsoftpay.com", "description": "Same host for test and live; the key selects the mode."}], "security": [{"bearerAuth": []}], "tags": [{"name": "Charges", "description": "Collect money (money-in)."}, {"name": "Payouts", "description": "Send money to a wallet (money-out). Full keys only."}, {"name": "Balance", "description": "Reconciliation."}, {"name": "Payment Links", "description": "Hosted checkout / QR."}, {"name": "Vending", "description": "Machine-present payments."}, {"name": "Subaccounts", "description": "Split payments."}], "paths": {"/v1/charges": {"post": {"tags": ["Charges"], "summary": "Create a charge", "description": "Prompt a customer's Mobile Money to pay. Returns immediately as `authorized`/`pending`; the final state arrives via the `charge.succeeded`/`charge.failed` webhook or by polling GET /v1/charges/{id}.", "security": [{"bearerAuth": []}], "parameters": [{"name": "X-Timestamp", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Unix time \u2014 seconds or Date.now() milliseconds. Requests older than 5 min (or >60s in the future) are rejected 400 (replay guard)."}, {"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Unique key per logical request. A retry with the same key returns the original result \u2014 never double-charges/double-pays."}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ChargeCreate"}}}}, "responses": {"201": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Charge"}}}}, "400": {"description": "Invalid request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "401": {"description": "Missing/invalid key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "409": {"description": "Idempotency key in flight", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "502": {"description": "Transient rail failure \u2014 retry with the same Idempotency-Key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}, "get": {"tags": ["Charges"], "summary": "List charges", "security": [{"bearerAuth": []}], "parameters": [{"name": "status", "in": "query", "required": false, "schema": {"type": "string", "enum": ["pending", "authorized", "succeeded", "failed", "refunded"]}, "description": "Filter by status"}, {"name": "reference", "in": "query", "required": false, "schema": {"type": "string"}, "description": "Filter by your merchant reference"}, {"name": "phone", "in": "query", "required": false, "schema": {"type": "string"}, "description": "Customer MSISDN (matched on last 9 digits)"}, {"name": "email", "in": "query", "required": false, "schema": {"type": "string"}, "description": "Customer email (case-insensitive)"}, {"name": "created_after", "in": "query", "required": false, "schema": {"type": "string"}, "description": "ISO-8601 lower bound"}, {"name": "created_before", "in": "query", "required": false, "schema": {"type": "string"}, "description": "ISO-8601 upper bound"}, {"name": "limit", "in": "query", "required": false, "schema": {"type": "integer"}, "description": "1\u2013100 (default 20)"}, {"name": "starting_after", "in": "query", "required": false, "schema": {"type": "string"}, "description": "Cursor: pass a previous next_cursor"}], "responses": {"200": {"description": "A page of results", "content": {"application/json": {"schema": {"type": "object", "properties": {"object": {"type": "string", "const": "list"}, "data": {"type": "array", "items": {"$ref": "#/components/schemas/Charge"}}, "has_more": {"type": "boolean"}, "next_cursor": {"type": ["string", "null"]}}}}}}, "401": {"description": "Auth", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/charges/{id}": {"get": {"tags": ["Charges"], "summary": "Retrieve a charge", "security": [{"bearerAuth": []}], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}, "example": "txn_\u2026"}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Charge"}}}}, "404": {"description": "Not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/charges/{id}/refund": {"post": {"tags": ["Charges"], "summary": "Refund a charge", "description": "Refunds the customer the FULL original amount via disbursement (the platform returns its charge fee to the merchant, so the merchant's net cost is only the payout fee). Requires a FULL key and available balance. Idempotent by design: a retry of an already-refunded charge returns 400 already_refunded (no Idempotency-Key needed). Split charges are not refundable yet (400).", "security": [{"bearerAuth": []}], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}, "example": "txn_\u2026"}, {"name": "X-Timestamp", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Unix time \u2014 seconds or Date.now() milliseconds. Requests older than 5 min (or >60s in the future) are rejected 400 (replay guard)."}], "responses": {"202": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RefundResult"}}}}, "400": {"description": "Already refunded / insufficient available / split charge", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "403": {"description": "Collections-only key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "404": {"description": "Not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/payouts": {"post": {"tags": ["Payouts"], "summary": "Create a payout", "description": "Disburse to a recipient's Mobile Money wallet. FULL key only (collections-only keys are 403'd). Fee is 1.5% (min UGX 200, cap UGX 5,000), same as collections \u2014 no flat fee; a failed payout refunds amount+fee. No charges beyond applicable URA taxation.", "security": [{"bearerAuth": []}], "parameters": [{"name": "X-Timestamp", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Unix time \u2014 seconds or Date.now() milliseconds. Requests older than 5 min (or >60s in the future) are rejected 400 (replay guard)."}, {"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Unique key per logical request. A retry with the same key returns the original result \u2014 never double-charges/double-pays."}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PayoutCreate"}}}}, "responses": {"201": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Payout"}}}}, "400": {"description": "Invalid / insufficient available balance", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "401": {"description": "Auth", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "403": {"description": "Collections-only key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "409": {"description": "Idempotency in flight", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "503": {"description": "Rail temporarily unavailable \u2014 retry same key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}, "get": {"tags": ["Payouts"], "summary": "List payouts", "security": [{"bearerAuth": []}], "parameters": [{"name": "status", "in": "query", "required": false, "schema": {"type": "string"}, "description": "Filter by status"}, {"name": "created_after", "in": "query", "required": false, "schema": {"type": "string"}, "description": "ISO-8601"}, {"name": "created_before", "in": "query", "required": false, "schema": {"type": "string"}, "description": "ISO-8601"}, {"name": "limit", "in": "query", "required": false, "schema": {"type": "integer"}, "description": "1\u2013100"}, {"name": "starting_after", "in": "query", "required": false, "schema": {"type": "string"}, "description": "Cursor: pass a previous next_cursor to page"}], "responses": {"200": {"description": "A page of results", "content": {"application/json": {"schema": {"type": "object", "properties": {"object": {"type": "string", "const": "list"}, "data": {"type": "array", "items": {"$ref": "#/components/schemas/Payout"}}, "has_more": {"type": "boolean"}, "next_cursor": {"type": ["string", "null"]}}}}}}, "401": {"description": "Auth", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/payouts/{id}": {"get": {"tags": ["Payouts"], "summary": "Retrieve a payout", "security": [{"bearerAuth": []}], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}, "example": "pout_\u2026"}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Payout"}}}}, "404": {"description": "Not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/payouts/bulk": {"post": {"tags": ["Payouts"], "summary": "Bulk payouts (per-item, not atomic)", "description": "Up to 1000 items; some may succeed while others fail. FULL key only. The array root may be `payouts` or `items`. Each item dedupes on its `reference`.", "security": [{"bearerAuth": []}], "parameters": [{"name": "X-Timestamp", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Unix time \u2014 seconds or Date.now() milliseconds. Requests older than 5 min (or >60s in the future) are rejected 400 (replay guard)."}, {"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Unique key per logical request. A retry with the same key returns the original result \u2014 never double-charges/double-pays."}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BulkPayoutCreate"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BulkPayoutResult"}}}}, "400": {"description": "Invalid", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "403": {"description": "Collections-only key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/me": {"get": {"tags": ["Balance"], "summary": "Identify the account behind the key", "description": "Returns the account's public identifiers (id/handle/public_key), the key's mode and scope, and KYC status. One call to label which account and mode a key belongs to.", "security": [{"bearerAuth": []}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Account"}}}}, "401": {"description": "Auth", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/balance": {"get": {"tags": ["Balance"], "summary": "Per-currency balance", "description": "Reports the JOURNAL sum (with a `consistent` flag if the cached figure disagrees). Mode-scoped by your key. Use `available` to decide if payroll can run.", "security": [{"bearerAuth": []}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BalanceResponse"}}}}, "401": {"description": "Auth", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/statements/{period}": {"get": {"tags": ["Balance"], "summary": "Monthly reconciliation statement", "description": "Finance-grade statement for a calendar month. `GET /v1/statements/2026-08` returns JSON; append `.pdf` (`/v1/statements/2026-08.pdf`) for a PDF. Each line carries YOUR reference, our id, and the MTN rail reference; opening/closing balances reconcile to GET /v1/balance. Mode-scoped by key.", "security": [{"bearerAuth": []}], "parameters": [{"name": "period", "in": "path", "required": true, "schema": {"type": "string"}, "example": "2026-08", "description": "YYYY-MM, optionally with a .pdf suffix."}], "responses": {"200": {"description": "Statement JSON (or a PDF when .pdf is used)", "content": {"application/json": {"schema": {"type": "object"}}, "application/pdf": {}}}, "400": {"description": "period must be YYYY-MM", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "401": {"description": "Auth", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/resolve-account": {"get": {"tags": ["Payouts"], "summary": "Resolve a Mobile-Money account (pre-payout check)", "security": [{"bearerAuth": []}], "parameters": [{"name": "phone", "in": "query", "required": true, "schema": {"type": "string"}, "description": "Recipient MSISDN"}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ResolveAccount"}}}}, "400": {"description": "Missing phone", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "403": {"description": "Collections-only key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/payment-links": {"post": {"tags": ["Payment Links"], "summary": "Create a hosted-checkout link + QR", "security": [{"bearerAuth": []}], "parameters": [{"name": "X-Timestamp", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Unix time \u2014 seconds or Date.now() milliseconds. Requests older than 5 min (or >60s in the future) are rejected 400 (replay guard)."}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaymentLinkCreate"}}}}, "responses": {"201": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PaymentLink"}}}}, "400": {"description": "Invalid (e.g. non-http success_url)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}, "get": {"tags": ["Payment Links"], "summary": "List payment links", "description": "Your links, newest first, mode-scoped to the key. Same cursor pagination as List charges. Each row includes transaction_status (null until the link is paid).", "security": [{"bearerAuth": []}], "parameters": [{"name": "reference", "in": "query", "required": false, "schema": {"type": "string"}, "description": "Filter by your merchant reference"}, {"name": "created_after", "in": "query", "required": false, "schema": {"type": "string"}, "description": "ISO-8601 lower bound"}, {"name": "created_before", "in": "query", "required": false, "schema": {"type": "string"}, "description": "ISO-8601 upper bound"}, {"name": "limit", "in": "query", "required": false, "schema": {"type": "integer"}, "description": "1\u2013100 (default 20)"}, {"name": "starting_after", "in": "query", "required": false, "schema": {"type": "string"}, "description": "Cursor: pass a previous next_cursor"}], "responses": {"200": {"description": "A page of results", "content": {"application/json": {"schema": {"type": "object", "properties": {"object": {"type": "string", "const": "list"}, "data": {"type": "array", "items": {"$ref": "#/components/schemas/PaymentLink"}}, "has_more": {"type": "boolean"}, "next_cursor": {"type": ["string", "null"]}}}}}}, "401": {"description": "Auth", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/vending/orders": {"post": {"tags": ["Vending"], "summary": "Create a vending order (QR + auto-dispense)", "security": [{"bearerAuth": []}], "parameters": [{"name": "X-Timestamp", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Unix time \u2014 seconds or Date.now() milliseconds. Requests older than 5 min (or >60s in the future) are rejected 400 (replay guard)."}, {"name": "Idempotency-Key", "in": "header", "required": false, "schema": {"type": "string"}, "description": "Unique key per logical request. A retry with the same key returns the original result \u2014 never double-charges/double-pays."}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/VendingOrderCreate"}}}}, "responses": {"201": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/VendingOrder"}}}}, "400": {"description": "Invalid", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "403": {"description": "Vending disabled", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/vending/orders/{id}": {"get": {"tags": ["Vending"], "summary": "Retrieve a vending order", "security": [{"bearerAuth": []}], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}, "example": "lnk_\u2026"}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/VendingOrder"}}}}, "404": {"description": "Not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/vending/conformance": {"post": {"tags": ["Vending"], "summary": "Machine Integration Standard \u2014 signature conformance check", "description": "Post a sample signed dispense-result callback; we confirm it verifies against your vendor signing profile. Moves no money, changes no state.", "security": [{"bearerAuth": []}], "parameters": [{"name": "X-Timestamp", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Unix time \u2014 seconds or Date.now() milliseconds. Requests older than 5 min (or >60s in the future) are rejected 400 (replay guard)."}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ConformanceCheck"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ConformanceResult"}}}}, "400": {"description": "Missing payload/sign or no vendor secret", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/scheduled-payouts": {"post": {"tags": ["Payouts"], "summary": "Create a recurring payout (payroll autopilot)", "description": "A recurring disbursement of a uniform amount to a list of recipients, daily/weekly/monthly. FULL key only. Fires on its interval; a cycle the merchant can't fully fund PAUSES (never a partial payroll); exactly-once per cycle.", "security": [{"bearerAuth": []}], "parameters": [{"name": "X-Timestamp", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Unix time \u2014 seconds or Date.now() milliseconds. Requests older than 5 min (or >60s in the future) are rejected 400 (replay guard)."}, {"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Unique key per logical request. A retry with the same key returns the original result \u2014 never double-charges/double-pays."}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ScheduledPayoutCreate"}}}}, "responses": {"201": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ScheduledPayout"}}}}, "400": {"description": "Invalid / cap breach", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "403": {"description": "Collections-only key", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "409": {"description": "Idempotency in flight", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}, "get": {"tags": ["Payouts"], "summary": "List scheduled payouts", "security": [{"bearerAuth": []}], "parameters": [{"name": "status", "in": "query", "required": false, "schema": {"type": "string"}, "description": "active | paused | cancelled"}, {"name": "limit", "in": "query", "required": false, "schema": {"type": "integer"}, "description": "1\u2013100"}], "responses": {"200": {"description": "A page of results", "content": {"application/json": {"schema": {"type": "object", "properties": {"object": {"type": "string", "const": "list"}, "data": {"type": "array", "items": {"$ref": "#/components/schemas/ScheduledPayout"}}, "has_more": {"type": "boolean"}, "next_cursor": {"type": ["string", "null"]}}}}}}, "401": {"description": "Auth", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/scheduled-payouts/{id}": {"get": {"tags": ["Payouts"], "summary": "Retrieve a scheduled payout", "security": [{"bearerAuth": []}], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}, "example": "spo_\u2026"}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ScheduledPayout"}}}}, "404": {"description": "Not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}, "/v1/subaccounts": {"post": {"tags": ["Subaccounts"], "summary": "Register a subaccount for split payments", "security": [{"bearerAuth": []}], "parameters": [{"name": "X-Timestamp", "in": "header", "required": true, "schema": {"type": "string"}, "description": "Unix time \u2014 seconds or Date.now() milliseconds. Requests older than 5 min (or >60s in the future) are rejected 400 (replay guard)."}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SubaccountCreate"}}}}, "responses": {"201": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Subaccount"}}}}, "400": {"description": "Invalid", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}}, "webhooks": {"charge.succeeded": {"post": {"summary": "charge.succeeded", "description": "Delivered to your webhook URL, signed with `X-Samsoftpay-Signature` (HMAC-SHA256 of the raw body using your whsec_ secret). Dedupe on `id`; may arrive more than once / out of order.", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"id": {"type": "string", "example": "evt_\u2026"}, "event": {"type": "string", "const": "charge.succeeded"}, "data": {"$ref": "#/components/schemas/Charge"}}}}}}, "responses": {"200": {"description": "Return any 2xx to acknowledge and stop retries."}}}}, "charge.failed": {"post": {"summary": "charge.failed", "description": "Delivered to your webhook URL, signed with `X-Samsoftpay-Signature` (HMAC-SHA256 of the raw body using your whsec_ secret). Dedupe on `id`; may arrive more than once / out of order.", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"id": {"type": "string", "example": "evt_\u2026"}, "event": {"type": "string", "const": "charge.failed"}, "data": {"$ref": "#/components/schemas/Charge"}}}}}}, "responses": {"200": {"description": "Return any 2xx to acknowledge and stop retries."}}}}, "charge.refunded": {"post": {"summary": "charge.refunded", "description": "Delivered to your webhook URL, signed with `X-Samsoftpay-Signature` (HMAC-SHA256 of the raw body using your whsec_ secret). Dedupe on `id`; may arrive more than once / out of order.", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"id": {"type": "string", "example": "evt_\u2026"}, "event": {"type": "string", "const": "charge.refunded"}, "data": {"$ref": "#/components/schemas/Charge"}}}}}}, "responses": {"200": {"description": "Return any 2xx to acknowledge and stop retries."}}}}, "payout.succeeded": {"post": {"summary": "payout.succeeded", "description": "Delivered to your webhook URL, signed with `X-Samsoftpay-Signature` (HMAC-SHA256 of the raw body using your whsec_ secret). Dedupe on `id`; may arrive more than once / out of order.", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"id": {"type": "string", "example": "evt_\u2026"}, "event": {"type": "string", "const": "payout.succeeded"}, "data": {"$ref": "#/components/schemas/Payout"}}}}}}, "responses": {"200": {"description": "Return any 2xx to acknowledge and stop retries."}}}}, "payout.failed": {"post": {"summary": "payout.failed", "description": "Delivered to your webhook URL, signed with `X-Samsoftpay-Signature` (HMAC-SHA256 of the raw body using your whsec_ secret). Dedupe on `id`; may arrive more than once / out of order.", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"id": {"type": "string", "example": "evt_\u2026"}, "event": {"type": "string", "const": "payout.failed"}, "data": {"$ref": "#/components/schemas/Payout"}}}}}}, "responses": {"200": {"description": "Return any 2xx to acknowledge and stop retries."}}}}, "vending.dispensed": {"post": {"summary": "vending.dispensed", "description": "Delivered to your webhook URL, signed with `X-Samsoftpay-Signature` (HMAC-SHA256 of the raw body using your whsec_ secret). Dedupe on `id`; may arrive more than once / out of order.", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"id": {"type": "string", "example": "evt_\u2026"}, "event": {"type": "string", "const": "vending.dispensed"}, "data": {"$ref": "#/components/schemas/VendingOrder"}}}}}}, "responses": {"200": {"description": "Return any 2xx to acknowledge and stop retries."}}}}, "vending.dispense_failed": {"post": {"summary": "vending.dispense_failed", "description": "Delivered to your webhook URL, signed with `X-Samsoftpay-Signature` (HMAC-SHA256 of the raw body using your whsec_ secret). Dedupe on `id`; may arrive more than once / out of order.", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"id": {"type": "string", "example": "evt_\u2026"}, "event": {"type": "string", "const": "vending.dispense_failed"}, "data": {"$ref": "#/components/schemas/VendingOrder"}}}}}}, "responses": {"200": {"description": "Return any 2xx to acknowledge and stop retries."}}}}, "dispute.opened": {"post": {"summary": "dispute.opened", "description": "Delivered to your webhook URL, signed with `X-Samsoftpay-Signature` (HMAC-SHA256 of the raw body using your whsec_ secret). Dedupe on `id`; may arrive more than once / out of order.", "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"id": {"type": "string", "example": "evt_\u2026"}, "event": {"type": "string", "const": "dispute.opened"}, "data": {"$ref": "#/components/schemas/Charge"}}}}}}, "responses": {"200": {"description": "Return any 2xx to acknowledge and stop retries."}}}}}, "components": {"securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer", "description": "`Authorization: Bearer sk_test_\u2026` (sandbox) or `sk_live_\u2026` (live). `sk_*_col_` collections-only keys can take money in but are 403'd on payouts/refunds."}}, "schemas": {"Error": {"type": "object", "properties": {"error": {"type": "string", "description": "Human-readable message."}, "code": {"type": "string", "description": "Stable machine-readable slug to branch on: bad_request, unauthorized, forbidden, not_found, conflict, rate_limited (domain slugs like already_refunded also appear here)."}, "request_id": {"type": "string", "description": "Echoes the X-Request-ID response header \u2014 quote it in support requests."}}}, "Account": {"type": "object", "properties": {"object": {"type": "string", "const": "account"}, "id": {"type": "string", "description": "Stable public identifier for this account (handle, or public key)."}, "handle": {"type": "string", "nullable": true}, "public_key": {"type": "string", "example": "pk_live_\u2026"}, "name": {"type": "string"}, "mode": {"type": "string", "enum": ["test", "live"]}, "scope": {"type": "string", "enum": ["full", "collections"]}, "kyc_status": {"type": "string", "enum": ["pending", "verified", "rejected"]}, "verified": {"type": "boolean"}, "webhook_configured": {"type": "boolean"}}}, "Customer": {"type": "object", "properties": {"phone": {"type": "string", "example": "256700123456"}, "email": {"type": "string"}}, "required": ["phone"]}, "Recipient": {"type": "object", "properties": {"phone": {"type": "string", "example": "256780000001"}, "name": {"type": "string"}}, "required": ["phone"]}, "Split": {"type": "object", "properties": {"subaccount": {"type": "string", "example": "sub_\u2026"}, "amount": {"type": "integer", "description": "Fixed share of the net (amount \u2212 fee)."}, "bps": {"type": "integer", "description": "Basis points of the net (100 = 1%)."}}, "required": ["subaccount"]}, "ChargeCreate": {"type": "object", "required": ["amount", "channel", "customer"], "properties": {"amount": {"type": "integer", "description": "Whole UGX (no minor units)."}, "currency": {"type": "string", "default": "UGX"}, "channel": {"type": "string", "enum": ["mtn_momo"], "description": "Required. mtn_momo is the only live rail."}, "customer": {"$ref": "#/components/schemas/Customer"}, "reference": {"type": "string", "description": "Your idempotent business reference."}, "split": {"type": "array", "items": {"$ref": "#/components/schemas/Split"}}}}, "Charge": {"type": "object", "properties": {"id": {"type": "string", "example": "txn_9f2c41d8a3b7e615"}, "status": {"type": "string", "enum": ["pending", "authorized", "succeeded", "failed", "refunded"]}, "amount": {"type": "integer", "description": "Whole UGX (no minor units)."}, "fee": {"type": "integer", "description": "Whole UGX (no minor units)."}, "currency": {"type": "string"}, "channel": {"type": "string"}, "mode": {"type": "string", "enum": ["test", "live"]}, "reference": {"type": ["string", "null"], "description": "Your merchant reference."}, "rail_reference": {"type": ["string", "null"]}, "failure_reason": {"type": ["string", "null"]}, "created_at": {"type": ["string", "null"], "format": "date-time"}, "completed_at": {"type": ["string", "null"], "format": "date-time"}, "settled": {"type": "boolean", "description": "True once swept from the hold to available."}, "available_on": {"type": ["string", "null"], "format": "date-time", "description": "When the funds become withdrawable."}}}, "RefundResult": {"type": "object", "properties": {"charge_id": {"type": "string"}, "status": {"type": "string", "const": "refunded"}, "refund": {"type": "object", "properties": {"id": {"type": "string", "example": "pout_\u2026"}, "amount": {"type": "integer", "description": "Whole UGX (no minor units)."}, "status": {"type": "string"}}}}}, "PayoutCreate": {"type": "object", "required": ["amount", "recipient"], "properties": {"amount": {"type": "integer", "description": "Whole UGX (no minor units)."}, "currency": {"type": "string", "default": "UGX"}, "channel": {"type": "string", "default": "mtn_momo"}, "recipient": {"$ref": "#/components/schemas/Recipient"}, "reference": {"type": "string"}}}, "Payout": {"type": "object", "properties": {"id": {"type": "string", "example": "pout_9f2a\u2026"}, "mode": {"type": "string", "enum": ["test", "live"]}, "status": {"type": "string", "enum": ["pending", "authorized", "succeeded", "failed"]}, "amount": {"type": "integer", "description": "Whole UGX (no minor units)."}, "fee": {"type": "integer", "description": "Whole UGX (no minor units)."}, "currency": {"type": "string"}, "channel": {"type": "string"}, "recipient_phone": {"type": ["string", "null"]}, "rail_reference": {"type": ["string", "null"]}, "reference": {"type": ["string", "null"]}, "failure_reason": {"type": ["string", "null"], "description": "Stable code, e.g. recipient_not_found, wallet_locked, timeout, insufficient_funds, ambiguous_network_error_pending_reconciliation."}, "created_at": {"type": ["string", "null"], "format": "date-time"}, "completed_at": {"type": ["string", "null"], "format": "date-time"}}}, "BulkPayoutCreate": {"type": "object", "properties": {"channel": {"type": "string", "default": "mtn_momo"}, "payouts": {"type": "array", "items": {"type": "object", "properties": {"amount": {"type": "integer", "description": "Whole UGX (no minor units)."}, "recipient": {"$ref": "#/components/schemas/Recipient"}, "phone": {"type": "string", "description": "Flat alternative to recipient.phone (CSV form)."}, "name": {"type": "string"}, "reference": {"type": "string"}}}}, "items": {"type": "array", "description": "Alias for `payouts`.", "items": {"type": "object"}}}}, "BulkPayoutResult": {"type": "object", "properties": {"batch_id": {"type": "string"}, "total": {"type": "integer"}, "accepted": {"type": "integer"}, "failed": {"type": "integer"}, "results": {"type": "array", "items": {"type": "object", "properties": {"index": {"type": "integer"}, "ok": {"type": "boolean"}, "id": {"type": "string"}, "status": {"type": "string"}, "reference": {"type": "string"}, "error": {"type": "string"}, "replayed": {"type": "boolean"}, "retryable": {"type": "boolean"}}}}}}, "BalanceResponse": {"type": "object", "properties": {"balances": {"type": "array", "items": {"type": "object", "properties": {"currency": {"type": "string"}, "available": {"type": "integer", "description": "Whole UGX (no minor units)."}, "pending": {"type": "integer", "description": "Whole UGX (no minor units)."}, "total": {"type": "integer", "description": "Whole UGX (no minor units)."}, "cached": {"type": "integer", "description": "Whole UGX (no minor units)."}}}}, "consistent": {"type": "boolean"}}}, "ResolveAccount": {"type": "object", "properties": {"msisdn": {"type": "string"}, "active": {"type": "boolean"}, "registered_name": {"type": ["string", "null"]}}}, "PaymentLinkCreate": {"type": "object", "required": ["amount"], "properties": {"amount": {"type": "integer", "description": "Whole UGX (no minor units)."}, "currency": {"type": "string", "default": "UGX"}, "reference": {"type": "string"}, "success_url": {"type": "string", "description": "Must be http(s)."}, "cancel_url": {"type": "string", "description": "Must be http(s)."}}}, "PaymentLink": {"type": "object", "properties": {"id": {"type": "string", "example": "lnk_\u2026"}, "amount": {"type": "integer", "description": "Whole UGX (no minor units)."}, "currency": {"type": "string"}, "status": {"type": "string"}, "qr_png_url": {"type": "string"}, "qr_svg_url": {"type": "string"}}}, "VendingOrderCreate": {"type": "object", "required": ["machine", "amount", "goods"], "properties": {"machine": {"type": "string", "description": "Machine number (jqbh)."}, "amount": {"type": "integer", "description": "Whole UGX (no minor units)."}, "currency": {"type": "string", "default": "UGX"}, "goods": {"type": "array", "items": {"type": "object"}}, "reference": {"type": "string"}, "success_url": {"type": "string"}}}, "VendingOrder": {"type": "object", "properties": {"order_id": {"type": "string", "example": "lnk_\u2026"}, "amount": {"type": "integer", "description": "Whole UGX (no minor units)."}, "currency": {"type": "string"}, "machine": {"type": "string"}, "payment_status": {"type": "string"}, "vending_status": {"type": "string"}, "qr_png_url": {"type": "string"}, "qr_svg_url": {"type": "string"}, "pay_url": {"type": "string"}, "status_url": {"type": "string"}}}, "ConformanceCheck": {"type": "object", "required": ["payload", "sign"], "properties": {"payload": {"type": "object", "description": "The exact dispense-result callback JSON."}, "timestamp": {"type": "string"}, "sign": {"type": "string"}}}, "ConformanceResult": {"type": "object", "properties": {"ok": {"type": "boolean"}, "vendor": {"type": "string"}, "profile": {"type": "string"}, "expected_reqData_any_of": {"type": "array", "items": {"type": "string"}}}}, "SubaccountCreate": {"type": "object", "required": ["name"], "properties": {"name": {"type": "string"}, "payout_phone": {"type": "string"}, "external_ref": {"type": "string"}}}, "ScheduledPayoutCreate": {"type": "object", "required": ["amount", "interval", "recipients"], "properties": {"amount": {"type": "integer", "description": "Paid to EACH recipient, per cycle."}, "currency": {"type": "string", "default": "UGX"}, "channel": {"type": "string", "default": "mtn_momo"}, "interval": {"type": "string", "enum": ["daily", "weekly", "monthly"]}, "recipients": {"type": "array", "items": {"$ref": "#/components/schemas/Recipient"}}, "name": {"type": "string", "description": "Optional label, e.g. 'August payroll'."}, "max_per_recipient": {"type": "integer", "description": "Optional per-recipient cap; a larger amount is rejected 400."}}}, "ScheduledPayout": {"type": "object", "properties": {"id": {"type": "string", "example": "spo_\u2026"}, "mode": {"type": "string", "enum": ["test", "live"]}, "status": {"type": "string", "enum": ["active", "paused", "cancelled"]}, "name": {"type": ["string", "null"]}, "amount": {"type": "integer", "description": "Whole UGX (no minor units)."}, "currency": {"type": "string"}, "channel": {"type": "string"}, "interval": {"type": "string"}, "recipients": {"type": "array", "items": {"$ref": "#/components/schemas/Recipient"}}, "max_per_recipient": {"type": ["integer", "null"]}, "next_run_at": {"type": ["string", "null"], "format": "date-time"}, "last_run_at": {"type": ["string", "null"], "format": "date-time"}, "failure_reason": {"type": ["string", "null"]}, "created_at": {"type": ["string", "null"], "format": "date-time"}}}, "Subaccount": {"type": "object", "properties": {"id": {"type": "string", "example": "sub_\u2026"}, "name": {"type": "string"}, "status": {"type": "string"}}}}}}