# Quickstart

This page takes one test payment from start to finish:

1. get your API key and webhook secret,
2. create a payment,
3. the customer pays on the checkout page,
4. the webhook (IPN) arrives,
5. your server verifies it and marks the order paid.

Everything runs in **test mode**: no coins move, and you
[simulate](/docs/testing) what the customer pays. Each step is shown with
`curl`, and the whole server at the end with the
[Node.js and Python libraries](#the-whole-integration).

## 1. Get your keys

Open an account (or ask your account owner for a test key):

```bash
curl -s https://api.payitgo.co/merchant/signup \
  -H 'Content-Type: application/json' \
  -d '{"merchant_name": "Acme Store", "email": "you@acme.example", "password": "a-long-password"}'
```

The `201` response holds two API keys, **shown only this once**:

```json
{
  "api_keys": [
    { "api_key": { "id": "4f2a9c1e7b3d5a08c6e1d7b2", "livemode": false, "…": "…" }, "secret": "4f9c2a7e1b8d3f6a0c5e9b2d7a4f1c8e3b6d9a2f5c8e1b4d7a0f3c6e9b2d5a8f" },
    { "api_key": { "id": "8d3b6e2f1a4c9e07b5d2a8c4", "livemode": true, "…": "…" }, "secret": "ea2bc4d2247c7159eec631a51a086a4a6bbd156cedb9982ea99510c5a9f3e0f5" }
  ],
  "…": "…"
}
```

Keep both on your server. Use the test key (the one with `"livemode": false`)
for now:

```bash
export PAYITGO_BASE_URL=https://api.payitgo.co
export PAYITGO_KEY=4f9c2a7e1b8d3f6a0c5e9b2d7a4f1c8e3b6d9a2f5c8e1b4d7a0f3c6e9b2d5a8f
```

(The full signup response: [Account and team](/docs/account#open-an-account).)

## 2. Get your webhook secret and set your URL

Your test webhook already exists, with its signing secret:

```bash
curl -s $PAYITGO_BASE_URL/v1/webhook -H "Authorization: Bearer $PAYITGO_KEY"
```

```json
{
  "webhook": {
    "livemode": false,
    "url": null,
    "secret": "6e86b3f1…a8418ae0",
    "created_at": "2026-10-10T07:48:44.128Z",
    "updated_at": "2026-10-10T07:48:44.128Z"
  }
}
```

Keep the secret on your server too (`PAYITGO_WEBHOOK_SECRET`), then tell payitgo
where to send notifications. The URL must be public and `https://` (for a
laptop, use a tunnel such as ngrok):

```bash
curl -s -X PUT $PAYITGO_BASE_URL/v1/webhook \
  -H "Authorization: Bearer $PAYITGO_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url": "https://acme.example/payitgo/ipn"}'
```

`200 OK` with the same object, now with `"url": "https://acme.example/payitgo/ipn"`.
Check that your endpoint is reachable with `POST /v1/webhook/test`: a signed
`ping` arrives within seconds ([Webhooks](/docs/webhooks#send-a-test)).

## 3. Create a payment

When your customer clicks "Pay with crypto", your server creates a payment for
the order:

```bash
curl -s $PAYITGO_BASE_URL/v1/payments \
  -H "Authorization: Bearer $PAYITGO_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"order_id": "ORD-1001", "amount_usd": "19.99", "success_url": "https://acme.example/thanks"}'
```

`201 Created`, shortened:

```json
{
  "id": "c41e7a9d2b05f3e8a6d17c90",
  "order_id": "ORD-1001",
  "status": "pending",
  "token": "",
  "token_options": [
    { "token": "USDT_TRC20", "chain": "tron", "min_usd": "6.44", "network_fee_usd": "3.22", "fee_note": "Final in about a minute." },
    { "token": "USDT_BEP20", "chain": "bsc", "min_usd": "1", "network_fee_usd": "0.01", "fee_note": "Final in under a minute." }
  ],
  "livemode": false,
  "usd_amount": "19.99",
  "amount_due_usd": "19.99",
  "expires_at": "2026-10-10T10:00:00.832Z",
  "checkout_url": "https://pay.example.com/pay/b28d4f61e9a03c75d2e8f1a6",
  "…": "…"
}
```

No `token` was given, so the customer will choose the coin on the checkout
page. Save `id` with your order. Sending the same `order_id` again returns
this same payment (`200`), so a retried request never makes a second one.
Every field: [the payment object](/docs/payments#the-payment-object).

## 4. The customer pays

Redirect the customer to `checkout_url`. The hosted page lets them choose a
coin, then shows the amount, the address and a QR code, counts down the 15
minutes and updates live.

No coins move in test mode: you choose what the customer does. Open
`checkout_url` yourself and choose a coin (or set it with
`POST /v1/payments/{id}/token`), then simulate the customer paying in full:

```bash
curl -s -X POST $PAYITGO_BASE_URL/v1/payments/c41e7a9d2b05f3e8a6d17c90/simulate \
  -H "Authorization: Bearer $PAYITGO_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"outcome": "confirmed"}'
```

`200 OK` with the payment, now with `"simulation": {"outcome": "confirmed", "state": "running"}`.
A few seconds later the deposit arrives and the checkout page moves to
`detecting`, then to `confirmed`, as it would for real coins. To start the
simulation without calling again, create the payment with
`"simulate": "confirmed"`: it plays as soon as the coin is chosen. Partial
payments, expiry, overpayment and the other outcomes:
[Test mode](/docs/testing#outcomes).

Never send real coins to a test payment's address: nothing watches it.

Want the amount, address and QR code on your own page, in your own design? See [Your own payment page](/docs/checkout#your-own-payment-page).

## 5. The webhook arrives

Your URL receives `payment.detecting` (money seen, confirming), then
`payment.confirmed` (final). In a simulation they come seconds apart; a live
payment's come as fast as its network confirms ([Currencies](/docs/currencies)):

```
POST /payitgo/ipn HTTP/1.1
Content-Type: application/json
User-Agent: payitgo-webhooks/1.0
X-Webhook-Id: 6a8c0e2b4d1f3a5c7e9b0d2f
X-Webhook-Event: payment.confirmed
X-Webhook-Attempt: 1
X-Webhook-Signature: t=1791619398,v1=5f1c…(64 hex)

{"created_at":"2026-10-10T09:04:03.771240512Z","data":{"id":"c41e7a9d2b05f3e8a6d17c90","order_id":"ORD-1001","status":"confirmed","livemode":false,…},"id":"2b8f4d6a0c1e3f5a7b9d2c4e","livemode":false,"type":"payment.confirmed"}
```

`data` is the payment. Every header and field:
[the notification](/docs/webhooks#the-notification).

## 6. Verify it and mark the order paid

Before trusting the notification, check its signature with your webhook
secret: `v1` must equal the hex HMAC-SHA256 of `t + "." + raw body`, and `t`
must be within 5 minutes. The libraries do this in one call (below);
[Webhooks](/docs/webhooks#verifying-the-signature) shows it by hand in five
languages.

Then:

- skip the event if you already processed its `id` (notifications can arrive
  twice);
- on `payment.confirmed`, find the order by `data.order_id` and mark it paid;
- answer `200` within 10 seconds. Anything else is retried, for up to a day
  and a half.

## The whole integration

### Node.js

```bash
npm install @payitgo/node express
```

```js
// server.js
import express from "express";
import { Payitgo } from "@payitgo/node";

const payitgo = new Payitgo(process.env.PAYITGO_KEY, {
  baseUrl: process.env.PAYITGO_BASE_URL,
  webhookSecret: process.env.PAYITGO_WEBHOOK_SECRET,
});
const app = express();

// 3. Create the payment and send the customer to pay.
app.post("/checkout", express.json(), async (req, res) => {
  const order = await orders.create(req.body); // your code
  const payment = await payitgo.payments.create({
    order_id: order.id,
    amount_usd: order.totalUsd, // "19.99"
    success_url: "https://acme.example/thanks",
    cancel_url: "https://acme.example/cart",
  });
  await orders.setPaymentId(order.id, payment.id);
  res.redirect(303, payment.checkout_url);
});

// 5–6. Receive the IPN, verify it, mark the order paid. Keep the body raw.
app.post("/payitgo/ipn", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = payitgo.webhooks.constructEvent(req.body, req.get("X-Webhook-Signature"));
  } catch {
    return res.sendStatus(400); // not from payitgo, or tampered with
  }
  if (!(await events.seen(event.id))) {
    if (event.type === "payment.confirmed") {
      await orders.markPaid(event.data.order_id); // idempotent
    }
    await events.remember(event.id);
  }
  res.sendStatus(200);
});

app.listen(3000);
```

### Python

```bash
pip install payitgo flask
```

```python
# server.py
import os
import payitgo
from flask import Flask, redirect, request

client = payitgo.Client(
    os.environ["PAYITGO_KEY"],
    base_url=os.environ["PAYITGO_BASE_URL"],
    webhook_secret=os.environ["PAYITGO_WEBHOOK_SECRET"],
)
app = Flask(__name__)

# 3. Create the payment and send the customer to pay.
@app.post("/checkout")
def checkout():
    order = orders.create(request.json)  # your code
    payment = client.payments.create(
        order_id=order.id,
        amount_usd=order.total_usd,  # "19.99"
        success_url="https://acme.example/thanks",
        cancel_url="https://acme.example/cart",
    )
    orders.set_payment_id(order.id, payment.id)
    return redirect(payment.checkout_url, code=303)

# 5-6. Receive the IPN, verify it, mark the order paid. Use the raw body.
@app.post("/payitgo/ipn")
def ipn():
    try:
        event = client.webhooks.construct_event(request.get_data(), request.headers.get("X-Webhook-Signature"))
    except payitgo.SignatureVerificationError:
        return "", 400  # not from payitgo, or tampered with
    if not events.seen(event.id):
        if event.type == "payment.confirmed":
            orders.mark_paid(event.data.order_id)  # idempotent
        events.remember(event.id)
    return "", 200
```

PHP, Go and Dart: [Libraries](/docs/sdks).

## Go live

1. The account owner turns live mode on: `POST /merchant/live {"enabled": true}`
   ([Account and team](/docs/account#live-mode)).
2. With the **live** key, read the live webhook's secret (`GET /v1/webhook`)
   and set its URL (`PUT /v1/webhook`). Live and test have separate URLs and
   separate secrets.
3. Deploy the live key and the live webhook secret. Remove any
   `simulate` from your payment requests: a live key refuses it.
4. Add a payout address for each coin you accept, so payments reach your
   wallet as they are. A coin you have none for is converted into one you
   have, which costs more
   ([Fees and settlement](/docs/settlement#coins-you-have-no-payout-address-for)).
5. Make one small real payment end to end.
