Docs / Getting started

Quickstart

Your first sandbox payment in 15 minutes. You'll need a Node.js, PHP, Python or Go server; there are examples for all four.

What you'll have at the endYour site creates an invoice, the customer pays on the Coinetic page (in the sandbox, with a button), and your server receives a webhook and fulfills the order exactly once.

1. Get sandbox keys

  1. Sign up in the sandbox dashboard and enable 2FA.
  2. “Keys & webhooks” → “Create key”: save the key_id (ak_…) and the secret.
  3. “Keys & webhooks” → “Add endpoint”: enter your handler URL, such as https://shop.example/webhooks/coinetic (in the sandbox, http://localhost:3000/… works too). Save the webhook secret.

Secrets are shown only once. Put them in your server's environment variables:

.env (never commit!)
API_KEY=ak_...
API_SECRET=...
WEBHOOK_SECRET=...

2. Download a client

Each client is a single file with no external dependencies: request signing, creating and fetching invoices, webhook verification. Download one for your language →

3. Create an invoice

Pass the price as a string, in USD, EUR, USDT or USDC. Derive the Idempotency-Key from the order number so a retry after a timeout returns the same invoice.

create-invoice.js
import { CoineticClient } from "./cryptopay.js";

const cp = new CoineticClient({ baseUrl: "https://sandbox.coinetic.money", keyId: process.env.API_KEY, secret: process.env.API_SECRET });

const invoice = await cp.createInvoice({
  order_id: "ORDER-1001",                 // your order number, unique per merchant
  price_amount: "25.00",                  // a string, not a number
  price_currency: "USD",
  return_url: "https://shop.example/orders/ORDER-1001",
  metadata: { user_id: "42" },
}, "order-ORDER-1001");                   // Idempotency-Key

console.log(invoice.checkout_url);        // https://sandbox.coinetic.money/pay/inv_…#t=…
create-invoice.php
require 'Cryptopay.php';

$cp = new \Cryptopay\Client('https://sandbox.coinetic.money', getenv('API_KEY'), getenv('API_SECRET'));

$invoice = $cp->createInvoice([
    'order_id'       => 'ORDER-1001',
    'price_amount'   => '25.00',
    'price_currency' => 'USD',
    'return_url'     => 'https://shop.example/orders/ORDER-1001',
    'metadata'       => ['user_id' => '42'],
], 'order-ORDER-1001');

echo $invoice['checkout_url'];
create_invoice.py
import os
from cryptopay import CoineticClient

cp = CoineticClient("https://sandbox.coinetic.money", os.environ["API_KEY"], os.environ["API_SECRET"])

invoice = cp.create_invoice({
    "order_id": "ORDER-1001",
    "price_amount": "25.00",
    "price_currency": "USD",
    "return_url": "https://shop.example/orders/ORDER-1001",
    "metadata": {"user_id": "42"},
}, "order-ORDER-1001")

print(invoice["checkout_url"])
main.go
cp := &cryptopayclient.Client{
	BaseURL: "https://sandbox.coinetic.money",
	KeyID:   os.Getenv("API_KEY"),
	Secret:  os.Getenv("API_SECRET"),
	HTTP:    &http.Client{Timeout: 10 * time.Second},
}
inv, err := cp.CreateInvoice(ctx, cryptopayclient.CreateInvoice{
	OrderID:       "ORDER-1001",
	PriceAmount:   "25.00",
	PriceCurrency: "USD",
	ReturnURL:     "https://shop.example/orders/ORDER-1001",
	Metadata:      map[string]string{"user_id": "42"},
}, "order-ORDER-1001")
if err != nil {
	return err
}
fmt.Println(inv.CheckoutURL)

4. Send the customer to checkout

Redirect the browser to invoice.checkout_url (HTTP 303). The customer picks a currency and sees the address, amount and QR code. In the sandbox, test payment buttons appear below the payment details: “Pay exact,” “Underpay 10%,” “Overpay 10%,” “Half amount” and “Pay remainder.”

Don't embed it in an iframeThe checkout page blocks embedding to protect against address spoofing. Open it in the same tab or in the app's system browser.

5. Receive the webhook

The order is always the same: verify the signature → drop duplicates → re-check the status via the API → fulfill the order once → respond with 2xx.

webhook.js
import express from "express";
import { verifyWebhook } from "./cryptopay.js";
// cp is the client from step 3. alreadyProcessed / markProcessed / fulfil are your own functions.

const app = express();

// express.raw: the signature is computed over the raw bytes, so don't use express.json() on this route
app.post("/webhooks/coinetic", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = verifyWebhook(process.env.WEBHOOK_SECRET, req.headers, req.body);
  } catch (e) {
    return res.status(e.status ?? 400).end();               // 401: invalid signature, 400: stale timestamp
  }
  try {
    const eventId = req.headers["x-webhook-id"];
    if (await alreadyProcessed(eventId)) return res.sendStatus(200);
    const inv = await cp.getInvoice(event.data.invoice_id); // status comes from the API, not the webhook
    if (inv.status === "paid" || inv.status === "overpaid") await fulfil(inv.order_id);
    await markProcessed(eventId);
    res.sendStatus(200);
  } catch (e) {
    console.error(e);
    res.sendStatus(500);                                    // non-2xx: Coinetic will retry delivery
  }
});

app.listen(3000);
webhook.php
require 'Cryptopay.php';

$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$raw = file_get_contents('php://input');                   // raw body, before json_decode
try {
    $event = \Cryptopay\verify_webhook(getenv('WEBHOOK_SECRET'), $headers, $raw);
} catch (\Cryptopay\CryptopayException $e) {
    http_response_code($e->status);
    exit;
}
if (already_processed($headers['x-webhook-id'])) { http_response_code(200); exit; }

$inv = $cp->getInvoice($event['data']['invoice_id']);       // status comes from the API
if (in_array($inv['status'], \Cryptopay\PAID_STATUSES, true)) {
    fulfil($inv['order_id']);
}
mark_processed($headers['x-webhook-id']);
http_response_code(200);
webhook.py
import os
from flask import Flask, request
from cryptopay import verify_webhook, CryptopayError, PAID_STATUSES

app = Flask(__name__)

@app.post("/webhooks/coinetic")
def webhook():
    headers = {k.lower(): v for k, v in request.headers.items()}
    try:
        event = verify_webhook(os.environ["WEBHOOK_SECRET"], headers, request.get_data())
    except CryptopayError as e:
        return "", e.status
    if already_processed(headers["x-webhook-id"]):
        return "", 200
    inv = cp.get_invoice(event["data"]["invoice_id"])      # status comes from the API
    if inv["status"] in PAID_STATUSES:
        fulfil(inv["order_id"])
    mark_processed(headers["x-webhook-id"])
    return "", 200
webhook.go
func webhook(w http.ResponseWriter, r *http.Request) {
	raw, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	if err := cryptopayclient.VerifyWebhook(os.Getenv("WEBHOOK_SECRET"), r.Header, raw, time.Now(), 5*time.Minute); err != nil {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}
	var ev struct {
		Data struct{ InvoiceID string `json:"invoice_id"` } `json:"data"`
	}
	if err := json.Unmarshal(raw, &ev); err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	inv, err := cp.GetInvoice(r.Context(), ev.Data.InvoiceID) // status comes from the API
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError) // Coinetic will retry
		return
	}
	if cryptopayclient.PaidStatuses[inv.Status] {
		fulfilOnce(inv.OrderID)
	}
	w.WriteHeader(http.StatusOK)
}

6. Bring the customer back

After payment, the page shows a “Return to store” button that takes the customer to your return_url. Show the order status there from your own database. If the webhook hasn't arrived yet, re-check with GET /v1/invoices/{id}.

Next steps