Malum API Reference

Accept card payments risk-free, receive crypto instantly, and build powerful merchant flows. This reference covers every live endpoint — authentication, transactions, payouts, payment links, products, storefronts and more.

Live   api.malum.co Base URL: https://malum.co Format: JSON Auth: MALUM header

Getting started #

The Malum API is a JSON-over-HTTPS interface. Every endpoint lives under https://malum.co and returns JSON. Authentication is handled through a single header — no OAuth, no signatures to calculate on every call.

Base URL

https://malum.co

A quick taste

Create a $9.99 USD payment and receive a hosted checkout URL.

curl -X POST https://malum.co/api/v2/payment/create \
  -H "MALUM: PXKGI625AG2:sec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 9.99,
    "currency": "USD",
    "customer_email": "[email protected]",
    "webhook_url": "https://yoursite.com/webhook",
    "success_url": "https://yoursite.com/thanks",
    "cancel_url":  "https://yoursite.com/cancelled"
  }'
const res = await fetch("https://malum.co/api/v2/payment/create", {
  method: "POST",
  headers: {
    "MALUM": "PXKGI625AG2:sec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    amount: 9.99,
    currency: "USD",
    customer_email: "[email protected]",
    webhook_url: "https://yoursite.com/webhook",
    success_url: "https://yoursite.com/thanks",
    cancel_url:  "https://yoursite.com/cancelled",
  }),
});
const data = await res.json();
console.log(data.link); // https://malum.co/checkout/TRN_XXXXXX
$ch = curl_init("https://malum.co/api/v2/payment/create");
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    "MALUM: PXKGI625AG2:sec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "Content-Type: application/json",
  ],
  CURLOPT_POSTFIELDS => json_encode([
    "amount" => 9.99,
    "currency" => "USD",
    "customer_email" => "[email protected]",
    "webhook_url" => "https://yoursite.com/webhook",
    "success_url" => "https://yoursite.com/thanks",
    "cancel_url"  => "https://yoursite.com/cancelled",
  ]),
]);
$res = json_decode(curl_exec($ch), true);
echo $res["link"];
200 OK
{
  "status": "success",
  "transaction_id": "TRN_6F0B93A9A2C6",
  "link": "https://malum.co/checkout/TRN_6F0B93A9A2C6",
  "bpf": false,
  "mgf": false,
  "timestamp": 1713705932
}

Authentication #

Authenticate every request by sending a MALUM header containing your Merchant ID and private key, separated by a colon. You can generate and rotate keys in the API section of your dashboard.

MALUM: {mid}:{private_key}
FieldFormatDescription
midstringYour Merchant ID (e.g. PXKGI625AG2).
private_keystringLive secret beginning with sec_, or sandbox secret starting with sbx_.
Keep secrets server-side. Private keys grant full access to your account. Never ship them in browser JavaScript, mobile apps, or public repositories.

Missing or invalid auth

200 / 401
{
  "status": "error",
  "message": "Invalid API Key."
}

Sandbox mode #

Every live key comes with a paired sandbox key prefixed with sbx_. Authenticate with the sandbox key and the entire stack switches into simulation: no real charges are taken, no crypto moves, and no webhooks are sent to third parties.

  • All write endpoints behave identically to production.
  • Transactions are flagged is_sandbox = 1.
  • Payouts are disabled in sandbox — use your live key for real withdrawals.
  • Use the sandbox controls endpoint to force a transaction to COMPLETED or CANCELLED.
Tip. Mirror your live integration exactly — use the same webhook handler, the same success/cancel URLs. Just swap the API key.

Account #

Manage your merchant balance, supported currencies, and crypto payouts.

GET /api/v3/account/balance Authenticated

Retrieve balance

Returns your available and pending balance (in USD).

curl https://malum.co/api/v3/account/balance \
  -H "MALUM: $MALUM_KEY"
const r = await fetch("https://malum.co/api/v3/account/balance", {
  headers: { "MALUM": process.env.MALUM_KEY },
});
const { data } = await r.json();
console.log(data.balance); // "1,204.55"
200 OK
{
  "status": "success",
  "message": "Balanced retrieved.",
  "data": {
    "balance":  "1,204.55",
    "pending":   "89.12",
    "currency": "USD"
  }
}
GET /api/v3/account/currencies Authenticated

List withdrawable currencies

Returns every crypto ticker + network you can currently withdraw to, along with minimum amounts and network fees.

curl https://malum.co/api/v3/account/currencies \
  -H "MALUM: $MALUM_KEY"
200 OK
{
  "status": "success",
  "message": "Currencies retrieved.",
  "data": [
    { "short": "USDT", "network": "TRON",     "min_withdraw": 5.00, "tx_fee": 1.00 },
    { "short": "USDC", "network": "BASE",     "min_withdraw": 2.00, "tx_fee": 0.10 },
    { "short": "BTC",  "network": "BITCOIN",  "min_withdraw": 10.0, "tx_fee": 2.50 }
  ]
}
POST /api/v3/account/payout Authenticated · Live only

Request a crypto payout

Debits your balance and dispatches a crypto transfer to the supplied wallet. 1 % platform fee applies, plus an instant-fee multiplier defined on your account.

Security rules enforced on this endpoint. 2FA must be enabled for at least 2 days, your account must not be under review, the wallet must not be sanctioned, and you must not have exceeded your daily withdrawal limit. Payouts are unavailable in sandbox mode.
FieldTypeDescription
amountrequirednumberUSD amount to withdraw. 1 < amount < 100 000.
currencyrequiredstringCrypto ticker, e.g. USDT, USDC, BTC.
networkrequiredstringNetwork identifier, e.g. TRON, BASE, BITCOIN.
addressrequiredstringDestination wallet address. Max 255 chars, no spaces.
curl -X POST https://malum.co/api/v3/account/payout \
  -H "MALUM: $MALUM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount":   125.00,
    "currency": "USDT",
    "network":  "TRON",
    "address":  "TXYZ123456789abcdefgHIJKLmnopQR"
  }'
await fetch("https://malum.co/api/v3/account/payout", {
  method: "POST",
  headers: {
    "MALUM": process.env.MALUM_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    amount:   125,
    currency: "USDT",
    network:  "TRON",
    address:  "TXYZ123456789abcdefgHIJKLmnopQR",
  }),
});
200 OK
{
  "status": "success",
  "message": "Withdrawal of 125.00 USD to TXYZ123… initiated.",
  "data": {
    "order_id": "PO_663f8e5c2a8d1",
    "link":     "https://malum.co/payout/5f2f3a04-..."
  }
}
200 Error
{ "status": "error", "message": "Insufficient funds." }
{ "status": "error", "message": "Daily withdrawal limit reached (3000/3000)." }
{ "status": "error", "message": "Currency or Network not supported." }
{ "status": "error", "message": "Travel Rule match detected. Payout is blocked pending verification." }
{ "status": "error", "message": "Payouts are not available in sandbox mode. Use your live API key." }

Payments #

Create hosted checkouts, recurring subscriptions, and look up their status.

POST /api/v2/payment/create Authenticated

Create a payment

Creates a one-off transaction and returns a hosted checkout URL where the customer completes payment via card, crypto or bank transfer.

FieldTypeDescription
amountrequirednumberAmount in currency. Min $0.50 USD equivalent, max $10,500 USD.
currencyrequiredstring3-character ISO code: USD, EUR, GBP, …
customer_emailrequiredstringCustomer's email. Must be a deliverable address.
webhook_urlrequiredstringYour server endpoint for payment events. Max 255 chars.
success_urlrequiredstringRedirect after successful payment.
cancel_urlrequiredstringRedirect if the customer cancels.
buyer_pays_feesoptionalboolAdd processor fees on top of the price, charged to the buyer.
merchant_pays_gw_feesoptionalboolYou absorb all gateway fees (min $10). Mutually exclusive with buyer_pays_fees.
metadataoptionalstringFree-text up to 254 chars, echoed back in webhooks.
product_titleoptionalstringDisplayed on the hosted checkout page.
product_descriptionoptionalstringShort description shown next to the title.
product_imageoptionalstringPublic URL for a thumbnail.
product_linkoptionalstringDeep link (e.g. to your own product page).
product_markdownoptionalstringMarkdown body rendered on the checkout page.
curl -X POST https://malum.co/api/v2/payment/create \
  -H "MALUM: $MALUM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount":          49.99,
    "currency":        "USD",
    "customer_email":  "[email protected]",
    "webhook_url":     "https://yoursite.com/webhooks/malum",
    "success_url":     "https://yoursite.com/order/success",
    "cancel_url":      "https://yoursite.com/order/cancelled",
    "buyer_pays_fees": true,
    "metadata":        "order=1042",
    "product_title":   "Pro subscription",
    "product_image":   "https://yoursite.com/img/pro.png"
  }'
const r = await fetch("https://malum.co/api/v2/payment/create", {
  method: "POST",
  headers: {
    "MALUM": process.env.MALUM_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    amount: 49.99,
    currency: "USD",
    customer_email: "[email protected]",
    webhook_url: "https://yoursite.com/webhooks/malum",
    success_url: "https://yoursite.com/order/success",
    cancel_url:  "https://yoursite.com/order/cancelled",
    buyer_pays_fees: true,
    metadata: "order=1042",
  }),
});
const { link, transaction_id } = await r.json();
// Redirect the customer:
window.location = link;
$ch = curl_init("https://malum.co/api/v2/payment/create");
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    "MALUM: " . getenv("MALUM_KEY"),
    "Content-Type: application/json",
  ],
  CURLOPT_POSTFIELDS => json_encode([
    "amount" => 49.99,
    "currency" => "USD",
    "customer_email" => "[email protected]",
    "webhook_url" => "https://yoursite.com/webhooks/malum",
    "success_url" => "https://yoursite.com/order/success",
    "cancel_url"  => "https://yoursite.com/order/cancelled",
    "buyer_pays_fees" => true,
  ]),
]);
$res = json_decode(curl_exec($ch));
header("Location: " . $res->link);
200 OK
{
  "status": "success",
  "transaction_id": "TRN_6F0B93A9A2C6",
  "link": "https://malum.co/checkout/TRN_6F0B93A9A2C6",
  "bpf": true,
  "mgf": false,
  "timestamp": 1713705932
}
POST /api/v2/payment/create_subscription Authenticated

Create a subscription

Creates a recurring subscription that re-bills the customer every billing_period_in_days days and sends them a payment reminder payment_reminder days before renewal.

FieldTypeDescription
customer_emailrequiredstringSubscriber's email address.
amountrequirednumberRecurring amount. Min 4.99, max 4999.99.
currencyrequiredstring3-character ISO code.
billing_period_in_daysrequiredint1 – 365. How often the customer is billed.
payment_reminderrequiredintDays before renewal a reminder email is sent. Must be ≤ billing period.
webhook_urlrequiredstringYour server endpoint for subscription events.
success_urlrequiredstringPost-payment redirect.
cancel_urlrequiredstringCancel redirect.
buyer_pays_feesrequired0 | 1Add fees on top for the buyer.
metadatarequiredstringFree-text up to 254 chars (can be empty).
curl -X POST https://malum.co/api/v2/payment/create_subscription \
  -H "MALUM: $MALUM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_email":         "[email protected]",
    "amount":                 19.99,
    "currency":               "USD",
    "billing_period_in_days": 30,
    "payment_reminder":       3,
    "webhook_url":            "https://yoursite.com/webhooks/malum",
    "success_url":            "https://yoursite.com/subscribed",
    "cancel_url":             "https://yoursite.com/cancelled",
    "buyer_pays_fees":        0,
    "metadata":               "plan=pro"
  }'
200 OK
{
  "status":  "success",
  "message": "Subscription created.",
  "data": { "checkout_id": "TRN_8F2AB94A7123" }
}
GET /api/lookup/{txid} Public

Look up a transaction

Returns the public state of a transaction, plus the payment methods available on its hosted checkout. Useful for rendering your own order status page.
Also available as /api/v2/payment/lookup?txid=....

FieldTypeDescription
txidrequiredpathThe transaction ID, e.g. TRN_6F0B93A9A2C6.
curl https://malum.co/api/lookup/TRN_6F0B93A9A2C6
200 OK
{
  "status": "success",
  "data": {
    "tid": "TRN_6F0B93A9A2C6",
    "amount": "49.99",
    "currency": "USD",
    "amount_in_usd": "49.99",
    "status": "PROCESSING",
    "business_info": {
      "business_name": "Acme Inc.",
      "business_logo_uri": "https://cdn.malum.co/logos/acme.png"
    },
    "payment_methods": {
      "card":  { "alt": "Visa / Mastercard", "url": "https://malum.co/checkout/TRN_.../card" },
      "usdt":  { "alt": "Tether (TRC20)",    "url": "https://malum.co/checkout/TRN_.../usdt" }
    }
  }
}
POST /api/v3/checkout/form Signed (no header)

Create a payment from an HTML form

Lets you wire up a plain <form> that posts directly to Malum and lands the customer on the hosted checkout — no JavaScript required. Sign each form server-side with your private key.

Build the signature as:

$sig = md5(
  $amount . $currency . $webhook_url . $success_url . $cancel_url .
  $customer_email . $buyer_pays_fees . $metadata . $business_id .
  $private_key
);

If you set any product_* field, also compute signed_product_hash:

$sph = md5(
  $product_markdown . $product_title . $product_image .
  $product_description . $product_link . $private_key
);
<form method="POST" action="https://malum.co/api/v3/checkout/form">
  <input type="hidden" name="amount" value="49.99">
  <input type="hidden" name="currency" value="USD">
  <input type="hidden" name="customer_email" value="[email protected]">
  <input type="hidden" name="webhook_url" value="https://yoursite.com/wh">
  <input type="hidden" name="success_url" value="https://yoursite.com/ok">
  <input type="hidden" name="cancel_url"  value="https://yoursite.com/no">
  <input type="hidden" name="business_id" value="PXKGI625AG2">
  <input type="hidden" name="signed_message" value="{{ $sig }}">
  <button type="submit">Pay</button>
</form>
Response. On success Malum issues an HTTP redirect directly to https://malum.co/checkout/{transaction_id}. The customer never sees the raw endpoint.
GET /api/v3/checkout/sandbox Sandbox only

Complete or cancel a sandbox transaction

Only works on transactions flagged is_sandbox = 1. Use it to simulate a successful or cancelled payment without going through a payment processor.

FieldTypeDescription
payTokenrequiredquerySandbox transaction ID.
actionrequiredquerycomplete or cancel.
# Force-complete a sandbox transaction
curl "https://malum.co/api/v3/checkout/sandbox?payToken=TRN_SANDBOX&action=complete"

On success you are redirected to the transaction's success_url or cancel_url.

Products #

Sell one-off digital goods with automatic fulfilment. Products support three delivery types: text_message (custom message), link (redirect URL), and serial_key (consume from a stock of pre-uploaded keys).

Create product #

POST /api/v3/products/create

Body

FieldTypeDescription
title requiredstringMax 255 chars.
price requiredfloat$0.50–$10,500 USD equivalent.
currency requiredstring3-char ISO code.
product_type requiredenumtext_message, link, or serial_key.
delivery_content requiredstringMessage body, redirect URL, or first serial key.
descriptionstringOptional long description.
instructionsstringPost-purchase instructions.
image_pathstringPath returned by /upload-image.
webhook_url, success_url, cancel_urlURLOptional lifecycle URLs.
buyer_pays_fees, merchant_pays_gw_feesboolFee handling, mutually exclusive.
show_on_storefrontboolList on your public storefront. Default true.
curl -X POST https://malum.co/api/v3/products/create \
  -H "MALUM: 12345:sk_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Ebook: Tactical Crypto",
    "price": 14.99,
    "currency": "USD",
    "product_type": "link",
    "delivery_content": "https://files.example.com/ebook.pdf",
    "instructions": "Download within 24h, link expires afterwards.",
    "show_on_storefront": true
  }'
const res = await fetch("https://malum.co/api/v3/products/create", {
  method: "POST",
  headers: {
    "MALUM": "12345:sk_live_xxxxxxxxxxxx",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    title: "Ebook: Tactical Crypto",
    price: 14.99,
    currency: "USD",
    product_type: "link",
    delivery_content: "https://files.example.com/ebook.pdf"
  })
});
const data = await res.json();
$ch = curl_init("https://malum.co/api/v3/products/create");
curl_setopt_array($ch, [
  CURLOPT_POST           => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER     => [
    "MALUM: 12345:sk_live_xxxxxxxxxxxx",
    "Content-Type: application/json"
  ],
  CURLOPT_POSTFIELDS => json_encode([
    "title"            => "Ebook: Tactical Crypto",
    "price"            => 14.99,
    "currency"         => "USD",
    "product_type"     => "link",
    "delivery_content" => "https://files.example.com/ebook.pdf",
  ]),
]);
$res = json_decode(curl_exec($ch), true);

Response 200 OK

{
  "status": "success",
  "pid": "PROD_8A3k2xN1",
  "slug": "ebook-tactical-crypto",
  "link_slug": "ebook-tactical-crypto-8a3k",
  "link": "https://malum.co/link/ebook-tactical-crypto-8a3k",
  "timestamp": 1713699305
}

Get product #

GET /api/v3/products/get?pid=PROD_XXX

Query parameters

FieldTypeDescription
pid requiredstringProduct identifier.
curl "https://malum.co/api/v3/products/get?pid=PROD_8A3k2xN1" \
  -H "MALUM: 12345:sk_live_xxxxxxxxxxxx"
Serial keys. For serial_key products, the response hides delivery_content and instead returns a live stock_count.

Update product #

POST /api/v3/products/update

Same body schema as create, with pid required and everything else overwriting existing values.

Toggle product #

POST /api/v3/products/toggle

Enable or disable the product. Disabled products 404 on the storefront.

curl -X POST https://malum.co/api/v3/products/toggle \
  -H "MALUM: 12345:sk_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"pid":"PROD_8A3k2xN1"}'

Delete product #

DELETE /api/v3/products/delete

Removes the product. Historical purchases remain in the transactions log.

curl -X DELETE https://malum.co/api/v3/products/delete \
  -H "MALUM: 12345:sk_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"pid":"PROD_8A3k2xN1"}'

Upload product image #

POST /api/v3/products/upload-image

Multipart upload, field name image. Returns a path you can pass as image_path when creating or updating a product.

ConstraintValue
Max size5 MB
Allowed typesJPG, PNG, GIF, WEBP
curl -X POST https://malum.co/api/v3/products/upload-image \
  -H "MALUM: 12345:sk_live_xxxxxxxxxxxx" \
  -F "image=@/path/to/cover.png"

Response 200 OK

{
  "status": "success",
  "url": "/assets/uploads/products/12345/8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d.png",
  "timestamp": 1713699512
}

Storefront #

Your public storefront at malum.co/store/{slug}. These endpoints control branding, media galleries, and which alternative payment methods are accepted directly from your own processor credentials.

Save storefront settings #

POST /api/v3/storefront/save

Partial update — only fields present in the body are written.

Body

FieldTypeDescription
shop_slugstringUnique URL slug (a–z, 0–9, _, -).
shop_namestringDisplay name, max 100 chars.
descriptionstringMax 2000 chars.
meta_title, meta_descriptionstringSEO metadata.
meta_image_path, logo_path, favicon_path, background_pathstringPaths returned by /upload.
ga_id, crisp_id, tawkto_idstringAnalytics / chat IDs.
social_discord, social_youtube, social_telegram, social_tiktok, social_instagramURLSocial links shown in the footer.
curl -X POST https://malum.co/api/v3/storefront/save \
  -H "MALUM: 12345:sk_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_slug": "mystore",
    "shop_name": "My Store",
    "description": "Premium digital goods, shipped instantly."
  }'

Upload storefront asset #

POST /api/v3/storefront/upload

Multipart upload, field name image. Returns a path suitable for logo/favicon/background fields.

ConstraintValue
Max size5 MB
Allowed typesJPG, PNG, GIF, WEBP, ICO
curl -X POST https://malum.co/api/v3/storefront/upload \
  -H "MALUM: 12345:sk_live_xxxxxxxxxxxx" \
  -F "image=@/path/to/logo.png"

Media pictures #

POST /api/v3/storefront/media-pictures

Replace the storefront gallery. Maximum 30 pictures.

Body

FieldTypeDescription
enabledboolShow the gallery on the storefront.
picturesstring[]Array of upload paths, in display order.
{
  "enabled": true,
  "pictures": [
    "/assets/uploads/storefront/12345/a1b2c3.png",
    "/assets/uploads/storefront/12345/d4e5f6.png"
  ]
}

Media videos #

POST /api/v3/storefront/media-videos

Embed YouTube / Vimeo videos on your storefront. Maximum 20 URLs.

Body

FieldTypeDescription
enabledboolShow the video section.
videosstring[]Array of video URLs.

Payment methods #

GET /api/v3/storefront/payment-methods
POST /api/v3/storefront/payment-methods

Configure third-party gateways that run under your own merchant accounts. Supported: stripe, mollie, skrill, cryptomus, coinpayments.

Secrets are masked on GET. Values returned for secret_key, webhook_secret, ipn_secret, api_key and secret_word show only the last four characters. If you POST a masked value back, the server keeps the existing stored secret rather than overwriting it.

POST body

{
  "methods": {
    "stripe": {
      "is_enabled": true,
      "credentials": {
        "publishable_key": "pk_live_xxx",
        "secret_key":      "sk_live_xxx",
        "webhook_secret":  "whsec_xxx"
      }
    },
    "coinpayments": {
      "is_enabled": true,
      "credentials": {
        "merchant_id": "xxxxxx",
        "ipn_secret":  "xxxxxx"
      }
    }
  }
}

Utility #

Public helpers for exchange rates, fee calculations and metadata. Most are keyless.

Exchange rates #

GET /api/v2/exchange/{from}-{to}
GET /api/rates/{from}-{to}

Rates are refreshed hourly and calculated against USD. Both pretty-URL forms and the query-string form are supported.

Parameters

FieldTypeDescription
fromstring3-char ISO source currency.
tostring3-char ISO target currency.
curl https://malum.co/api/v2/exchange/USD-EUR
# or
curl https://malum.co/api/rates/USDEUR

Response 200 OK

{
  "status": "success",
  "from": "USD",
  "to": "EUR",
  "rate": 0.9124,
  "timestamp": 1713699721
}

Calculate fees #

GET /api/v2/service/calcfees

Preview the exact charge the buyer will see for a given transaction and payment method — useful before redirecting to checkout.

Query parameters

FieldTypeDescription
payToken requiredstringThe transaction ID.
payMethod requiredstringPayment method slug (see Coins).
curl "https://malum.co/api/v2/service/calcfees?payToken=TRN_xxx&payMethod=bitcoin"

Coins / payment methods #

GET /api/v2/service/coins

Returns the complete list of supported crypto coins and alternative payment methods, including their slugs, icons, and network names.

curl https://malum.co/api/v2/service/coins

Service fees #

GET /api/v2/service/servicefee

Returns per-payment-method processor fees (fixed + percentage + minimum).

IP lookup #

GET /api/v2/ip

GeoIP lookup with city, country and ASN — used by the Malum fraud engine, exposed for convenience.

Query parameters

FieldTypeDescription
ipstringIPv4 or IPv6. Defaults to the caller IP.
curl "https://malum.co/api/v2/ip?ip=8.8.8.8"

Discord bot link #

POST /api/v2/discord/link

Authenticates a Discord server against the Malum merchant account it was paired with during the bot install flow.

Body

FieldTypeDescription
GUID requiredstringDiscord server (guild) ID.
Key requiredstringOne-time pairing key issued in the merchant dashboard.

Response 200 OK

{
  "status": "success",
  "message": "linked successfully",
  "apiKey": "a1b2c3d4e5f6...",
  "timestamp": 1713699821
}

Reseller #

The Reseller API powers crypto-for-fiat rails built on top of Malum's liquidity. It does not require a MALUM API key — pricing is computed against USD and each order is scoped by a caller-supplied external_reference. All endpoints accept GET parameters.

Payment options #

GET /api/v3/reseller/payment_options

Price-check available crypto rails against a fiat amount.

Query parameters

FieldTypeDescription
fiat_amount requiredfloatValue between 0 and 10,000.
fiat_currency requiredstring3-char ISO code.
curl "https://malum.co/api/v3/reseller/payment_options?fiat_amount=150&fiat_currency=USD"

Create order #

GET /api/v3/reseller/checkout

Creates a pending reseller order. Returns a transaction_code (UUID4) used when requesting the payment link.

Query parameters

FieldTypeDescription
fiat_amount requiredfloat0<x≤10,000.
fiat_currency requiredstring2–4 letter ISO code.
crypto_currency requiredstringe.g. BTC, USDT.
crypto_currency_network requiredstringe.g. BTC, TRX, ETH.
wallet_address requiredstringDestination wallet for the customer.
external_reference requiredstringYour internal order ID.
webhook_urlURLOptional event receiver.
Partner split — all fields below must be supplied together or omitted entirely.
partner_wallet_addressstringPartner payout wallet.
partner_crypto_currencystringe.g. USDT.
partner_crypto_currency_networkstringe.g. TRX.
partner_comission_percentagefloatCommission to split off.
partner_external_referencestringPartner's internal ID.
partner_webhook_urlURLPartner webhook receiver.
curl "https://malum.co/api/v3/reseller/checkout\
?fiat_amount=200\
&fiat_currency=USD\
&crypto_currency=USDT\
&crypto_currency_network=TRX\
&wallet_address=TXYZ...\
&external_reference=ORDER-9921\
&webhook_url=https://yourapp.com/hooks/reseller"

Response 200 OK

{
  "status": "success",
  "message": "Order created successfully.",
  "data": {
    "transaction_code": "a1b2c3d4-e5f6-4789-9abc-deadbeefcafe",
    "fiat_amount": 200,
    "fiat_currency": "USD",
    "crypto_currency": "USDT",
    "crypto_currency_network": "TRX",
    "wallet_address": "TXYZ...",
    "status": "pending",
    "payment_options": [ ... ]
  }
}

Integrations #

Drop Malum into an existing checkout flow without building your own UI. Pick white-label for full JSON control or Shopify for a native payment app.

White-label checkout #

GET /api/v3/whitelabel/checkout/{payToken}

Returns everything needed to render your own checkout UI: business branding, currency amount, customer email and the list of payment methods available for the buyer's country (Cloudflare CF-IPCountry header, or explicit country override).

Query parameters

FieldTypeDescription
countrystringISO-2 override. Defaults to the caller's detected country.
curl "https://malum.co/api/v3/whitelabel/checkout/TRN_xxx?country=US"

Response 200 OK

{
  "status": "success",
  "message": "Checkout data fetched.",
  "data": {
    "business_logo_uri": "https://.../logo.png",
    "business_name":     "Acme Inc.",
    "currency_amount":   "49.99",
    "currency_symbol":   "USD",
    "customer_email":    "[email protected]",
    "payment_methods":   [ ... ]
  }
}
Collecting the payment. Once the buyer picks a method, submit the HTML form described in Checkout form (HTML) or open https://malum.co/checkout/{payToken} in a new tab.

Shopify #

Malum ships a native Shopify Payment App. These endpoints are called server-to-server by Shopify — you don't invoke them from your own code, but they're documented here for transparency and for self-hosted / dev-store installs.

POST /api/v3/shopify/payment-session

Triggered when the buyer selects Malum at Shopify checkout. Creates a Malum transaction and returns a redirect URL.

Headers

HeaderDescription
X-Shopify-Shop-DomainThe merchant's *.myshopify.com domain.
X-Shopify-Hmac-Sha256HMAC-SHA256 of the raw request body, base64-encoded, using the stored webhook_secret.

Body JSON from Shopify

{
  "id":                "pss_1a2b3c",
  "gid":               "gid://shopify/PaymentSession/pss_1a2b3c",
  "amount":            "49.99",
  "currency":          "USD",
  "merchantReference": "#1001",
  "customer":          { "email": "[email protected]" }
}
POST /api/v3/shopify/refund-session

Shopify initiates a refund. Malum issues a reversal on the original transaction.

POST /api/v3/shopify/webhook

Shopify lifecycle webhooks (uninstall, app deauthorization, GDPR data requests).

Errors & responses #

Every endpoint returns JSON. Success responses include "status": "success"; failures return an HTTP error code and a JSON body with status, error (or message) and a timestamp.

Error shape

{
  "status": "failed",
  "error": "currency is not supported.",
  "timestamp": 1713699999
}

Common HTTP codes

CodeMeaning
200Request succeeded.
400Bad request — missing or invalid parameter.
401Missing or invalid MALUM header.
403API key is valid but lacks permission for this resource.
404Resource not found (transaction, product, link…).
405Method not allowed — use the documented verb.
418Malformed or impossible IP / geo payload.
419Session / CSRF token invalid or expired.
429Rate limited — back off and retry.
500Malum-side error. Safe to retry with exponential backoff.
Idempotency. Payment create endpoints are not idempotent by default — store the returned txid and never re-send the same create call in a retry loop. For reliable retries, deduplicate on your own order ID before calling.

Webhooks #

Malum delivers webhook events as HTTP POST requests with a JSON body to the webhook_url you set on the transaction, payment link or product.

Payload

{
  "status":            "COMPLETED",
  "txn":               "TRN_xxxxxxxxx",
  "requested_amount":  9.99,
  "requested_currency":"USD",
  "cid":               "CST_xxxxxxxxx",
  "checkout":          9.99,
  "metadata":          "{\"order_id\":\"A-123\"}",
  "buyer_pays_fees":   0,
  "sandbox":           false,
  "timestamp":         1713699999,
  "signature":         "5f4dcc3b5aa765d61d8327deb882cf99"
}

Event types

StatusWhen it fires
CREATEDNew transaction registered but not paid.
PROCESSINGBuyer submitted payment, awaiting confirmations.
COMPLETEDPayment cleared — funds credited to the merchant balance.
EXPIREDBuyer never paid before the window elapsed.
CANCELLEDMerchant or buyer cancelled the checkout.

Signature verification

Every payload is signed with the merchant's webhook_key (or sandbox_webhook_key when sandbox=true). Compute:

signature = md5( txn + "|" + timestamp + "|" + webhook_key )
<?php
$payload = json_decode(file_get_contents('php://input'), true);

$expected = md5(
  $payload['txn'] . '|' .
  $payload['timestamp'] . '|' .
  getenv('MALUM_WEBHOOK_KEY')
);

if (!hash_equals($expected, $payload['signature'])) {
    http_response_code(401);
    exit('invalid signature');
}

// …mark order paid…
http_response_code(200);
import crypto from "crypto";
import express from "express";

const app = express();
app.use(express.json());

app.post("/webhooks/malum", (req, res) => {
  const { txn, timestamp, signature } = req.body;
  const expected = crypto
    .createHash("md5")
    .update(`${txn}|${timestamp}|${process.env.MALUM_WEBHOOK_KEY}`)
    .digest("hex");

  if (expected !== signature) return res.status(401).end();

  // …mark order paid…
  res.sendStatus(200);
});
Retries. Malum considers any 2xx response a success. Non-2xx responses are retried with exponential backoff for up to 24 hours. Respond quickly (<12 s) to avoid timeouts.