Official SDKs
Moonip publishes official clients for Node.js, Python, PHP and .NET. They wrap the same
/v1 REST API described in the API Reference — anything you can do
with curl, you can do without an SDK. What the SDKs add is the part that is easy to
get subtly wrong: idempotency, retry policy, and webhook signature verification.
All four share one version: 0.8.0.
| Language | Package | Install | Runtime |
|---|---|---|---|
| Node.js / TypeScript | @moonip/node | npm i @moonip/node | Node 18+ |
| Python | moonip | pip install moonip | Python 3.8+ |
| PHP | moonip/sdk-php | composer require moonip/sdk-php | PHP 8.1+ |
| .NET / C# | Moonip | dotnet add package Moonip | netstandard2.0, net8.0 |
#What every SDK guarantees
The four clients are behaviourally identical, not merely similar. The webhook signature verification in particular runs against a shared set of test vectors in CI — if Node accepts a signature, Python, PHP and .NET accept the same one, byte for byte.
#Resource families
Every client exposes the same six resource families — same routes, same wire shapes as the API Reference:
| Family | Node | Python | PHP | .NET |
|---|---|---|---|---|
Charges (/v1/charges) | client.charges | client.charges | $client->charges | client.Charges |
Payment operations (/v1/payments) | client.payments | client.payments | $client->payments | client.Payments |
Payouts (/v1/withdrawals) | client.payouts | client.payouts | $client->payouts | client.Payouts |
Balance (/v1/wallets) | client.balance | client.balance | $client->balance | client.Balance |
Webhooks (/v1/webhooks) | client.webhooks (incl. client.webhooks.endpoints) | client.webhooks (incl. client.webhooks.endpoints) | $client->webhooks (incl. $client->webhooks->endpoints) | client.Webhooks (incl. client.Webhooks.Endpoints) |
| MED cases | client.medCases | client.med_cases | $client->medCases | client.MedCases |
Method names follow each language's convention (client.payments.create(input, { idempotencyKey }) in Node, client.payments.create(input, idempotency_key) in Python, $client->payments->create($input, $idempotencyKey) in PHP, client.Payments.CreateAsync(input, idempotencyKey) in .NET). payments.refund is deliberately absent: the API answers 403 to every API-key request — error.code is "forbidden" and the message starts with the REFUND_ADMIN_ONLY marker (a message prefix, not a code), because refunds come from the admin panel.
#Authentication
Authorization: Bearer ak_live_…The SDKs send the canonical Authorization: Bearer header; the legacy apikey:
header is still accepted by the API for compatibility. The environment (test or
live) comes from the key itself. You never configure an environment, and no
account or merchant identifier is ever passed in or returned.
#Idempotency is required on financial writes
Creating a charge or a payout requires an idempotency key. The SDKs refuse the call without one rather than sending it and hoping.
Derive the key from your order (pedido-1234), never from a random value. Random
defeats the entire mechanism: if your request arrived but the response was lost,
retrying with a fresh key creates a second charge; retrying with the same key
returns the original.
#Retry policy
| Situation | Retried? |
|---|---|
GET / HEAD on 5xx or network error | yes |
POST with an idempotency key | yes — same key on every attempt |
POST without an idempotency key | never |
Any 4xx | never — the request is wrong, retrying will not fix it |
Backoff is exponential with full jitter, capped at 8s.
The third row is the one that matters. A network failure does not tell you whether the server processed the request — only that no response came back. Retrying a keyless financial write on that ambiguity is how a customer gets charged twice.
#Quickstart
#Node.js
import { MoonipClient } from '@moonip/node';
const moonip = new MoonipClient({ apiKey: process.env.MOONIP_API_KEY });
const charge = await moonip.charges.create(
{
amount: 15000, // R$ 150.00 in centavos — always an integer
currency: 'BRL',
payment_method: 'pix',
customer: { name: 'Maria Silva', document: '12345678901' },
},
{ idempotencyKey: `pedido-${orderId}` },
);
charge.pix.br_code; // Pix Copia e Cola#Python
import os
from moonip import MoonipClient
moonip = MoonipClient(api_key=os.environ["MOONIP_API_KEY"])
cobranca = moonip.charges.create(
{
"amount": 15000,
"currency": "BRL",
"payment_method": "pix",
"customer": {"name": "Maria Silva", "document": "12345678901"},
},
idempotency_key=f"pedido-{pedido_id}",
)
cobranca["pix"]["br_code"]The Python SDK has no runtime dependencies — standard library only. A payments SDK runs inside your process; every transitive package it pulls in becomes supply-chain surface you inherit from us.
#PHP
<?php
require __DIR__ . '/vendor/autoload.php';
use Moonip\MoonipClient;
$cliente = new MoonipClient(['apiKey' => getenv('MOONIP_API_KEY')]);
$cobranca = $cliente->charges->create([
'amount' => 15000,
'currency' => 'BRL',
'payment_method' => 'pix',
'customer' => ['name' => 'Maria Silva', 'document' => '12345678901'],
], 'pedido-' . $pedidoId);
$cobranca['pix']['br_code'];#C#
using Moonip;
// Register as a SINGLETON — it is thread-safe and reuses its HttpClient.
// One client per request exhausts TCP ports, the classic .NET trap.
var moonip = new MoonipClient(Environment.GetEnvironmentVariable("MOONIP_API_KEY")!);
var cobranca = await moonip.Charges.CreateAsync(new
{
amount = 15000,
currency = "BRL",
payment_method = "pix",
customer = new { name = "Maria Silva", document = "12345678901" },
}, idempotencyKey: $"pedido-{orderId}");
var brCode = cobranca!.RootElement.GetProperty("pix").GetProperty("br_code").GetString();#Verifying webhooks
Verification needs no API key — instantiate the webhook helper on its own.
// Node — Express with a raw body parser
app.post('/webhooks/moonip', express.raw({ type: 'application/json' }), (req, res) => {
if (!moonip.webhooks.verify(req.body, req.headers['x-moonip-signature'], secret)) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString('utf8'));
res.status(200).end(); // any 2xx confirms delivery
});# Python — Flask
if not webhooks.verify(request.get_data(), request.headers.get("X-Moonip-Signature"), secret):
return "", 401// C# — minimal API
if (!webhooks.Verify(corpo, req.Headers["X-Moonip-Signature"], secret))
return Results.Unauthorized();Pass the raw bytes. Do not deserialize and re-serialize the body before verifying: any difference in whitespace or key order changes the HMAC and the signature fails. This is the single most common webhook integration bug, in every language.
Signatures older than 5 minutes are rejected by default, which stops a captured POST from being replayed forever. See Webhooks for the signature format and the retry schedule.
#Errors
Every SDK raises typed errors carrying status, code, type and request_id.
| Class of failure | Node | Python | PHP | C# |
|---|---|---|---|---|
| 401 / 403 | MoonipAuthError | MoonipAuthError | MoonipAuthError | MoonipAuthException |
| 400 / 422 | MoonipValidationError | MoonipValidationError | MoonipValidationError | MoonipValidationException |
| 409 | MoonipConflictError | MoonipConflictError | MoonipConflictError | MoonipConflictException |
| No response at all | MoonipNetworkError | MoonipNetworkError | MoonipNetworkError | MoonipNetworkException |
Branch on code, never on the message — messages are written for humans and change
without notice. code is contract; see Errors for the catalogue. The
one documented message marker is REFUND_ADMIN_ONLY (refunds are admin-panel-only and
answer plain forbidden); match it as a prefix, not as a code.
The network error is deliberately not a subclass of the API error in any of the four SDKs. When no response came back, you do not know whether the server processed the request. Catching both in one branch hides exactly the distinction that decides whether retrying is safe.
Quote the request_id when you contact support — it locates the exact request in our
logs.