Server Integration
Integrate DonutMe payments directly using the REST API — no SDK required.
Overview
DonutMe provides a straightforward REST API for server-to-server integration. You can create checkout sessions, query transactions, and verify webhook signatures using standard HTTP requests in any language.
Coming Soon: A TypeScript SDK is planned for the future. In the meantime, use the REST API directly or leverage AI-assisted integration via our Copilot Skill.
Authentication
All authenticated API requests use your API key in the X-API-Key header:
-pink-400">curl -X GET https:-lime-400">"text-neutral-500">//api.donutme.xyz/api/v1/transactions \
-H -lime-400">"X-API-Key: dm_live_your_api_key_here"Generate API keys from the Dashboard → Account → API Keys.
See Authentication for full details on scopes and key management.
Create a Checkout Session
The checkout session endpoint is public (no API key required) — your server or client can create sessions directly:
class="text-pink-400">class=class="text-pink-400">class="text-lime-400">"text-neutral-500">// Node.js / TypeScript example
class="text-pink-400">const response = class="text-pink-400">await class="text-sky-400">fetch(class="text-pink-400">class="text-lime-400">"https:class="text-pink-400">class="text-neutral-500class="text-pink-400">class="text-lime-400">">//api.donutme.xyz/api/v1/checkout/sessions", {
method: class="text-pink-400">class="text-lime-400">"POST",
headers: { class="text-pink-400">class="text-lime-400">"Content-Type": class="text-pink-400">class="text-lime-400">"application/json" },
body: class="text-sky-400">JSON.stringify({
paymentPlanId: class="text-pink-400">class="text-lime-400">"your-payment-plan-id",
email: class="text-pink-400">class="text-lime-400">"customer@example.com", class="text-pink-400">class=class="text-pink-400">class="text-lime-400">"text-neutral-500">// optional
metadata: { orderId: class="text-pink-400">class="text-lime-400">"order_12345" }, class="text-pink-400">class=class="text-pink-400">class="text-lime-400">"text-neutral-500">// optional
}),
});
class="text-pink-400">const { data } = class="text-pink-400">await response.json();
class="text-pink-400">class=class="text-pink-400">class="text-lime-400">"text-neutral-500">// data.token → use to build checkout class="text-sky-400">URL
class="text-pink-400">class=class="text-pink-400">class="text-lime-400">"text-neutral-500">// Redirect user to: https://donutme.xyz/en/pay/{planId}/checkout?session={data.token}Python
import requests
resp = requests.post("https:">//api.donutme.xyz/api/v1/checkout/sessions", json={
"paymentPlanId": "your-payment-plan-id",
"email": "customer@example.com",
})
session = resp.json()["data"]
checkout_url = f"https:">//donutme.xyz/en/pay/{plan_id}/checkout?session={session['token']}"Query Transactions
Requires API key with transactions:read scope:
class="text-pink-400">const response = class="text-pink-400">await class="text-sky-400">fetch(
class="text-pink-400">class="text-lime-400">"https:class="text-pink-400">class="text-neutral-500class="text-pink-400">class="text-lime-400">">//api.donutme.xyz/api/v1/projects/{projectId}/transactions?status=confirmed&page=1&limit=20",
{ headers: { class="text-pink-400">class="text-lime-400">"X-API-Key": class="text-pink-400">class="text-lime-400">"dm_live_xxx" } }
);
class="text-pink-400">const { data, meta } = class="text-pink-400">await response.json();
class="text-pink-400">class=class="text-pink-400">class="text-lime-400">"text-neutral-500">// data: Transaction[]
class="text-pink-400">class=class="text-pink-400">class="text-lime-400">"text-neutral-500">// meta.pagination: { page, limit, total, totalPages }Verify Webhook Signatures
When receiving webhooks, verify the HMAC-SHA256 signature to ensure authenticity:
class="text-pink-400">import crypto class="text-pink-400">from class="text-pink-400">class="text-lime-400">"node:crypto";
class="text-pink-400">function verifyWebhookSignature(
payload: string,
signatureHeader: string,
secret: string
): boolean {
class="text-pink-400">class=class="text-pink-400">class="text-lime-400">"text-neutral-500">// Header format: class="text-pink-400">class="text-lime-400">"v1,sha256=<class="text-sky-400">hex-digest>"
class="text-pink-400">const match = signatureHeader.match(/^v1,sha256=([0-9a-f]{64})$/);
class="text-pink-400">if (!match) class="text-pink-400">return class="text-pink-400">false;
class="text-pink-400">const received = match[1];
class="text-pink-400">const expected = crypto
.createHmac(class="text-pink-400">class="text-lime-400">"sha256", secret)
.update(payload, class="text-pink-400">class="text-lime-400">"utf8")
.digest(class="text-pink-400">class="text-lime-400">"hex");
class="text-pink-400">return crypto.timingSafeEqual(
class="text-sky-400">Buffer.class="text-pink-400">from(received),
class="text-sky-400">Buffer.class="text-pink-400">from(expected)
);
}
class="text-pink-400">class=class="text-pink-400">class="text-lime-400">"text-neutral-500">// In your webhook handler:
app.post(class="text-pink-400">class="text-lime-400">"/webhook", (req, res) => {
class="text-pink-400">const signature = req.headers[class="text-pink-400">class="text-lime-400">"x-donutme-signature"];
class="text-pink-400">const isValid = verifyWebhookSignature(
class="text-sky-400">JSON.stringify(req.body),
signature,
class="text-sky-400">process.env.WEBHOOK_SECRET
);
class="text-pink-400">if (!isValid) {
class="text-pink-400">return res.status(401).json({ error: class="text-pink-400">class="text-lime-400">"Invalid signature" });
}
class="text-pink-400">const { event, data } = req.body;
switch (event) {
case class="text-pink-400">class="text-lime-400">"payment.confirmed":
class="text-pink-400">class=class="text-pink-400">class="text-lime-400">"text-neutral-500">// Fulfill the order
break;
case class="text-pink-400">class="text-lime-400">"payment.failed":
class="text-pink-400">class=class="text-pink-400">class="text-lime-400">"text-neutral-500">// Handle failure
break;
}
res.status(200).json({ received: class="text-pink-400">true });
});Python
import hmac
import hashlib
import re
def verify_signature(payload: bytes, signature_header: str, secret: str) -> bool:
"""Verify DonutMe webhook signature (format: v1,sha256=<hex>)."""
match = re.match(r"^v1,sha256=([0-9a-f]{64})$", signature_header)
if not match:
return False
received = match.group(1)
expected = hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(received, expected)Error Handling
All API responses use a consistent envelope:
"text-neutral-500">// Success
{
"success": true,
"statusCode": 200,
"data": { ... },
"meta": { "timestamp": "...", "requestId": "req_..." }
}
"text-neutral-500">// Error
{
"success": false,
"statusCode": 422,
"message": "Validation failed",
"error": { "code": "VALIDATION_ERROR", "details": [...] }
}Common error codes:
| Code | Description |
|---|---|
VALIDATION_ERROR | Invalid request body or parameters |
RESOURCE_NOT_FOUND | Plan, session, or transaction not found |
RATE_LIMITED | Too many requests — check Retry-After header |
INSUFFICIENT_SCOPES | API key lacks required scope |
PAYMENT_PLAN_INACTIVE | Plan is not active |
PROJECT_SUSPENDED | Project is suspended or archived |
Rate Limits
| Endpoint Category | Limit |
|---|---|
| Checkout session creation | 5 req/min per IP |
| Authenticated reads (list/get) | 60 req/min per key |
| Authenticated writes | 30 req/min per key |
| Webhook verification | Unlimited |
When rate limited, the response includes a Retry-After header (in seconds).
Next Steps
- Embed Checkout — iframe integration with postMessage events
- Webhooks — Full webhook event reference and payload format
- API Authentication — API key scopes and management
- API Payments — Payment-specific endpoints