API reference

A JSON REST API. Create a checkout session on your server, send the buyer to the hosted page, and fulfill the order when the webhook arrives. Payments settle into your own Square account.

Base URL: https://YOUR-VINGATE-DOMAIN/api/v1

Quick start

  1. Sign up, connect Square, and copy a secret key from Developers in the dashboard.
  2. Create a checkout session and redirect the buyer to its checkout_url.
  3. Handle checkout.session.completed at your webhook endpoint and fulfill the order.
curl https://YOUR-VINGATE-DOMAIN/api/v1/checkout/sessions \
  -H "Authorization: Bearer vg_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1001" \
  -d '{
    "amount": 2500,
    "currency": "USD",
    "description": "Order #1001",
    "success_url": "https://yourstore.com/thanks",
    "cancel_url": "https://yourstore.com/cart",
    "metadata": { "order_id": "1001" }
  }'

Authentication

Send your secret key as a bearer token. vg_test_ keys use the Square sandbox (no real money). vg_live_ keys take real payments and need an active plan. Never use secret keys in a browser or app.

Authorization: Bearer vg_live_...

Roll a key from the dashboard to rotate it: the old key keeps working for 24 hours.

A key belongs to one business. If your account has several businesses, each store uses its own business's key, and its payments go to that business's Square account.

Account

GET/account

The business a key belongs to, and whether it is finished: business_name, mode, plan, cloaked, payments_ready (at least one account can take volume in this mode), gateways, and how many webhook endpoints exist. Read-only, no secrets.

Meant for a setup screen: one call proves the key and says what is missing. A live key with no active plan still answers, so402 here means "finish billing", not "wrong key". The WooCommerce plugin uses it.

curl https://YOUR-VINGATE-DOMAIN/api/v1/account -H "Authorization: Bearer vg_live_..."

Checkout sessions

POST/checkout/sessions

  • amount (required): integer cents, at least 100.
  • description (required): shown on the checkout page.
  • success_url, cancel_url (required): the buyer returns to success_url?session_id=cs_....
  • currency: USD (default).
  • line_items: [{ name, quantity, unit_amount }], must add up to amount.
  • customer_email, metadata (up to 20 string keys, sent back in webhooks).
  • Idempotency-Key header: retries with the same key return the same session.

Returns 201 with the session. Sessions expire after 30 minutes.

{
  "id": "cs_9dQ2...",
  "object": "checkout.session",
  "livemode": false,
  "status": "open",
  "amount": 2500,
  "currency": "USD",
  "checkout_url": "https://YOUR-VINGATE-DOMAIN/c/cs_9dQ2...",
  "payment": null,
  "metadata": { "order_id": "1001" },
  "expires_at": "2026-10-12T19:30:00.000Z"
}

GET/checkout/sessions/:id

Status is open, processing, complete or expired. Includes payment once paid.

GET/checkout/sessions

Payments

GET/payments/:id

GET/payments

Each payment has status, amount, amount_refunded, card brand and last 4, and the checkout session id.

Refunds

POST/refunds

payment_id (required), amount (cents, default: the remaining balance), reason, metadata. Returns status pending; refund.succeeded or refund.failed follows.

curl https://YOUR-VINGATE-DOMAIN/api/v1/refunds \
  -H "Authorization: Bearer vg_test_..." -H "Content-Type: application/json" \
  -d '{ "payment_id": "pay_...", "amount": 1000, "reason": "Damaged item" }'

GET/refunds

Front sites

A front site is a second site the processor is shown instead of the store. Pairing is two hosts and a one-time code: this endpoint pings the site, and returns an approve_url the merchant opens on that site. Nothing is written until its own admin approves, and a cloaked business cannot mint a Stripe account without a confirmed pair.

POST/pairs

front_url (https), mode (adopt writes the front site’s name and address onto the business, present writes the address alone) and site (your store’s own address). Returns the pair and approve_url; the code expires in ten minutes.

curl https://YOUR-VINGATE-DOMAIN/api/v1/pairs \\
  -H "Authorization: Bearer vg_test_..." -H "Content-Type: application/json" \\
  -d '{ "front_url": "https://shop.example", "mode": "adopt", "site": "https://peptides.example" }'

The front site redeems the code at POST /pairs/confirm (no key) with its own front_url, name and optional logo_url; only the origin the code was made for is accepted, and the answer carries a revoke token it uses at POST /pairs/:id/revoke to remove itself. GET /pairs/preview?code= is the consent payload both screens render.

GET/pairs

GET/pairs/:id

DELETE/pairs/:id

Removes the pairing and restores what it wrote: the front address it set, and its subdomain. An address typed by hand survives. Cloaked sessions name the store again from the next one.

Webhooks

POST/webhooks/endpoints

url (public https), optional events. Returns a whsec_ signing secret once. Endpoints get test and live events; check livemode.

GET/webhooks/endpoints

DELETE/webhooks/endpoints/:id

Turns an endpoint off. The delivery log keeps its history and stops collecting failures. Removing one that is already off returns 404. The WooCommerce plugin calls this when it is disconnected or the store changes address.

Events:

  • checkout.session.completed
  • checkout.session.expired
  • payment.succeeded
  • payment.failed
  • refund.succeeded
  • refund.failed
  • dispute.created
  • dispute.resolved
  • payout.completed

Every delivery has a VinGate-Signature: t=<unix>,v1=<hex> header: an HMAC-SHA256 of <t>.<raw body> with your secret. Reject timestamps older than 5 minutes. Delivery is at least once, so dedupe on event.id. Failed deliveries retry 7 times over about 32 hours; you can resend from the dashboard.

// Node.js (Express): use the raw body, not parsed JSON
const crypto = require("crypto");
app.post("/webhooks/vingate", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("VinGate-Signature") || "";
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", process.env.VINGATE_WEBHOOK_SECRET)
    .update(`${parts.t}.${req.body}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  const ok = fresh && parts.v1 && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
  if (!ok) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  if (event.type === "checkout.session.completed") fulfill(event.data.object.metadata.order_id);
  res.sendStatus(200);
});
# Python (Flask)
import hmac, hashlib, time, json
@app.post("/webhooks/vingate")
def vingate_webhook():
    parts = dict(p.split("=", 1) for p in request.headers.get("VinGate-Signature", "").split(","))
    body = request.get_data(as_text=True)
    expected = hmac.new(SECRET.encode(), f"{parts.get('t')}.{body}".encode(), hashlib.sha256).hexdigest()
    if abs(time.time() - int(parts.get("t", 0))) > 300 or not hmac.compare_digest(expected, parts.get("v1", "")):
        return "", 400
    event = json.loads(body)
    return "", 200

Pagination

List endpoints return newest first: { "object": "list", "data": [...], "has_more": true }. Pass limit (1 to 100, default 10) and starting_after (the last id from the previous page).

curl "https://YOUR-VINGATE-DOMAIN/api/v1/payments?limit=50&starting_after=pay_..." -H "Authorization: Bearer vg_live_..."

Errors

{ "error": { "type": "invalid_request_error", "message": "amount: Too small", "param": "amount" } }
  • 400 invalid_request_error: bad parameters.
  • 401 authentication_error: missing, invalid, revoked or expired key.
  • 402 permission_error: live key without an active plan.
  • 404 not_found_error.
  • 429 rate_limit_error.
  • 500 api_error: retry with the same Idempotency-Key.

Rate limits

Live keys: 120 requests per minute per key. Responses include X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 includes Retry-After. Test keys are not rate limited.

Testing

Test keys use the Square sandbox. Use any future expiry date and any ZIP unless noted.

ScenarioCardDetails
Success (Visa)4111 1111 1111 1111CVV 111
Success (Mastercard)5105 1051 0510 5100CVV 111
Success (Amex)3400 000000 00009CVV 1111
Declined4000 0000 0000 0002Card declined
Wrong CVVAny success cardCVV 911
Wrong ZIP (AVS)Any success cardZIP 99999
Bad expirationAny success cardExpiry 01/40
3D Secure challenge4310 0000 0020 1019Code 123456
3D Secure, no challenge4800 0000 0000 0004CVV 111
3D Secure fails4811 1100 0000 0008Verification fails

Use the dashboard's "Send test event" button to check your webhook handler without making a payment.