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
- Sign up, connect Square, and copy a secret key from Developers in the dashboard.
- Create a checkout session and redirect the buyer to its
checkout_url. - Handle
checkout.session.completedat 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 tosuccess_url?session_id=cs_....currency:USD(default).line_items:[{ name, quantity, unit_amount }], must add up toamount.customer_email,metadata(up to 20 string keys, sent back in webhooks).Idempotency-Keyheader: 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.completedcheckout.session.expiredpayment.succeededpayment.failedrefund.succeededrefund.faileddispute.createddispute.resolvedpayout.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 "", 200Pagination
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" } }400invalid_request_error: bad parameters.401authentication_error: missing, invalid, revoked or expired key.402permission_error: live key without an active plan.404not_found_error.429rate_limit_error.500api_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.
| Scenario | Card | Details |
|---|---|---|
| Success (Visa) | 4111 1111 1111 1111 | CVV 111 |
| Success (Mastercard) | 5105 1051 0510 5100 | CVV 111 |
| Success (Amex) | 3400 000000 00009 | CVV 1111 |
| Declined | 4000 0000 0000 0002 | Card declined |
| Wrong CVV | Any success card | CVV 911 |
| Wrong ZIP (AVS) | Any success card | ZIP 99999 |
| Bad expiration | Any success card | Expiry 01/40 |
| 3D Secure challenge | 4310 0000 0020 1019 | Code 123456 |
| 3D Secure, no challenge | 4800 0000 0000 0004 | CVV 111 |
| 3D Secure fails | 4811 1100 0000 0008 | Verification fails |
Use the dashboard's "Send test event" button to check your webhook handler without making a payment.