Payment Gateway API

Accept credit/debit cards (Visa, Mastercard, Maestro), Apple Pay, Google Pay, and SEPA bank transfers on any website. No merchant onboarding required. No API keys. Instant payouts in USDC (Polygon) to your own wallet.

No API Keys No Merchant KYC API v1.1 Instant USDC Payouts Visa & Mastercard Apple Pay Google Pay SEPA
Merchant vs. End-User KYC Merchants can integrate instantly without any sign-up, documents, or verification. End-users (customers) may be asked to complete a quick identity check by the payment provider during their first transaction, depending on the provider and jurisdiction.

Integration Flow

1 server-side API call + 1 client-side redirect + callback webhook. Optionally, a currency conversion call before the redirect.

1

Create Wallet (server-side API call)

Generate a temporary encrypted wallet address by passing your USDC wallet + a callback URL. You receive an encrypted address_in and an ipn_token for tracking. Store the ipn_token in your database.

2

Redirect Customer (client-side redirect)

Redirect the customer's browser to the checkout URL with the encrypted wallet, amount, currency, and provider. The customer completes payment via the provider's hosted checkout.

3

Receive & Verify Callback (webhook)

After payment, a GET callback hits your URL with transaction data. Always verify via the Payment Status endpoint before marking the order as paid. USDC arrives instantly in your wallet.

Base URL

API: https://pay.syncom-europe.com
Checkout: https://checkout.syncom-europe.com
Request types /control/* — server-side HTTP GET requests. No authentication or API keys required.
/process-payment.php — browser redirect, not a server-to-server API call. Redirect the user's browser via HTTP 302 or window.location.

URL Encoding Rules

Incorrect URL encoding is the most common integration error. Follow these rules precisely.

Option A (recommended): URLSearchParams (JS) or http_build_query (PHP) These handle encoding automatically. Do not apply encodeURIComponent or urlencode to any value — the library does it for you. This eliminates double-encoding bugs entirely.
Choose one approach — do not mix Either use URLSearchParams/http_build_query (Option A) or manual encoding (Option B). Mixing them causes double-encoding, which is the #1 cause of "Invalid address" errors.

Option B: Manual Encoding (only if not using URLSearchParams)

If you build URL strings manually, apply encodeURIComponent() exactly once to each dynamic value:

ParameterEncoding Rule
callback Apply encodeURIComponent() once to the full callback URL (including query params).
address_in May contain /, =, +. Apply encodeURIComponent() once.
email Apply encodeURIComponent() once — @ becomes %40.

JavaScript — Recommended (URLSearchParams)

JavaScript
// Create Wallet — URLSearchParams encodes automatically const walletUrl = new URL("https://pay.syncom-europe.com/control/wallet.php"); walletUrl.searchParams.set("address", merchantWallet); walletUrl.searchParams.set("callback", `https://myshop.com/api/callback?orderId=${orderId}`); const res = await fetch(walletUrl.toString()); const wallet = await res.json(); // Process Payment — same approach, no manual encoding needed const checkoutUrl = new URL("https://checkout.syncom-europe.com/process-payment.php"); checkoutUrl.searchParams.set("address", wallet.address_in); checkoutUrl.searchParams.set("amount", amount); checkoutUrl.searchParams.set("provider", provider); checkoutUrl.searchParams.set("currency", currency); checkoutUrl.searchParams.set("email", email); // Redirect customer's browser window.location.href = checkoutUrl.toString();

PHP Example

PHP
// Create Wallet — http_build_query handles encoding $params = http_build_query([ 'address' => $merchantWallet, 'callback' => "https://myshop.com/orders.php?number={$orderId}", ]); $walletUrl = "https://pay.syncom-europe.com/control/wallet.php?{$params}"; $wallet = json_decode(file_get_contents($walletUrl), true); // Process Payment — redirect customer $checkoutParams = http_build_query([ 'address' => $wallet['address_in'], 'amount' => $amount, 'provider' => $provider, 'currency' => $currency, 'email' => $email, ]); header("Location: https://checkout.syncom-europe.com/process-payment.php?{$checkoutParams}");

Amount & Precision

To avoid floating-point rounding issues, follow these rules when handling amounts.

Treat all amounts as strings Never use floating-point arithmetic on payment amounts. The API returns all values as strings — keep them as strings in your code and database.
FieldWhat it representsWhen to use
value_coin USDC amount received by the order's temporary wallet from the payment provider. Use for payment verification — compare against your expected order amount.
value_forwarded_coin Total USDC forwarded from the order wallet to the merchant (after network fees). Use for accounting — this is what actually arrives in your wallet.

When verifying payments, allow a small tolerance (e.g. ±0.02 USDC) to account for minor FX rounding by providers.


GET Create Wallet

Generate a temporary encrypted wallet address for an order. Each request generates a new temporary wallet + ipn_token. Your integration must track existing sessions to avoid creating duplicates for the same order. All received USDC is instantly forwarded to your merchant wallet.

https://pay.syncom-europe.com/control/wallet.php?address={your-usdc-wallet}&callback={callback-url}
Server-side only Control endpoints should only be called from your server, never from the browser. They create payment sessions and return ipn_token in responses, which must remain secret.

Request Parameters

ParameterDescription
addressrequired Your USDC (Polygon) wallet address to receive instant payouts.
callbackrequired Your callback URL (must be HTTPS). Include your order identifier as a query parameter (e.g. ?orderId=123) — it will be preserved in the callback. If using URLSearchParams / http_build_query, pass directly. If building URLs manually, URL-encode once.

Example Request

cURL
curl --location \ 'https://pay.syncom-europe.com/control/wallet.php?address=0xF977814e90dA44bFA03b6295A0616a897441aceC&callback=https%3A%2F%2Fwww.example.com%2Forders.php%3Fnumber%3D8271468415326'

Response

FieldDescription
address_in Encrypted wallet address. Pass as the address param to Process Payment. If using URLSearchParams / http_build_query, pass directly — do not manually encode. If building URLs manually, apply encodeURIComponent() once.
polygon_address_in Unencrypted Polygon wallet address assigned to this order (for reference/on-chain tracking).
callback_url The decoded callback URL you provided.
ipn_token Secret token for verifying payment status. Store server-side in your database only — never expose in frontend code, client-side URLs, or logs.
Response — 200 OK
{ "address_in": "eE5A43DAkczRwxoW3IL7sGsRh6CiMx4kkTCccr6n/YMTGhy9b...", "polygon_address_in": "0x756C4D5EAad2165b3841a543Cf851Eed6AAF211B", "callback_url": "https://www.example.com/orders.php?number=8271468415326", "ipn_token": "ZEE2cW8zb1N0N2otZEc3eHh3MDNUU1lTMEExYmVvcD..." }
Idempotency warning Each call to Create Wallet generates a new wallet and ipn_token, even for the same callback URL. To avoid orphaned wallets on retries, always check your database for an existing session before calling this endpoint. See Idempotency.

REDIRECT Process Payment

Redirect the customer's browser to the payment page. This is not a server-side API call — send the customer here via HTTP redirect or window.location.

https://checkout.syncom-europe.com/process-payment.php?address={encrypted-address}&amount={amount}&provider={provider}&currency={currency}&email={email}

Request Parameters

ParameterDescription
addressrequired The address_in from Create Wallet. If using URLSearchParams / http_build_query, pass directly (no manual encoding). If building URLs manually, apply encodeURIComponent() once.
amountrequired Payment amount as a string (e.g. 103.78). Do not use floating-point math.
providerrequired Payment provider identifier. See providers list (e.g. moonpay, stripe, transak).
currencyrequired Fiat currency code (e.g. USD, EUR). Some providers support USD only — use the Convert endpoint first.
emailoptional Customer email. If using URLSearchParams / http_build_query, pass directly. If building URLs manually, encode once. Pre-fills the provider's checkout form.

Example

Browser Redirect URL
https://checkout.syncom-europe.com/process-payment.php ?address=eE5A43DAkczRwxoW3IL7sGsRh6CiMx4kkTCccr6n%2FYMTGhy9b1eeIJLTr9Lho64fJTIeOfgsnJNNc%2FarqtR1jw%3D%3D &amount=103.78 &provider=moonpay &email=john%40example.com &currency=USD
No iframes The customer must be redirected to the payment provider directly. Do NOT embed the checkout page in an iframe — this violates provider terms and will cause payment failures.

GET Convert to USD

Some providers (Stripe, Wert, Transfi, Ramp Network) support USD only. Use this endpoint to convert other currencies before calling Process Payment.

https://pay.syncom-europe.com/control/convert.php?from={currency}&value={amount}

Request Parameters

ParameterDescription
fromrequired Source currency code (e.g. EUR, SEK, GBP, CHF).
valuerequired Amount to convert (e.g. 1258.31).

Example

cURL
curl --location 'https://pay.syncom-europe.com/control/convert.php?from=EUR&value=1258.31'
Response — 200 OK
{ "status": "success", "value_coin": "1351.76", "exchange_rate": "1.07427" }

GET Callback Event

When a customer completes payment, a GET request is sent to your callback URL with all your original query parameters plus the following transaction data.

Don't confuse the two address_in values The address in the checkout URL is the encrypted address_in from Create Wallet. The address_in in the callback query string is the unencrypted Polygon wallet and equals polygon_address_in from Create Wallet.
https://www.example.com/orders.php?number=827746841326&value_coin=105.6&coin=polygon_usdc&txid_in=0xa22a...&txid_out=0x94c2...&address_in=0x32e8...&value_forwarded_coin=104.016

Callback Parameters

ParameterDescription
value_coin USDC amount paid by the provider to the order wallet.
coin Payout coin type, usually polygon_usdc or polygon_usdt.
txid_in Polygon TXID — payment from provider to the order's temporary wallet.
txid_out Polygon TXID — instant payout from temporary wallet to your merchant wallet.
address_in Unencrypted Polygon wallet for this order. Must match polygon_address_in from Create Wallet.
value_forwarded_coin Total USDC forwarded to the merchant wallet (after network fees).
Never trust callbacks blindly Callbacks can be spoofed — do not use value_coin, txid_out, or any callback query parameters for settlement. Treat the callback only as a trigger. Always verify via the Callback Verification flow before marking an order as paid.

GET Check Payment Status

Verify whether a payment has been completed. Use the ipn_token stored during Create Wallet.

https://pay.syncom-europe.com/control/payment-status.php?ipn_token={ipn_token}

Request Parameters

ParameterDescription
ipn_tokenrequired The ipn_token from the Create Wallet response.

Response

FieldDescription
status paid or unpaid
value_coin USDC amount sent by the provider to the order wallet.
txid_out Blockchain TXID of the payout to your merchant wallet.
coin Payout coin type (e.g. polygon_usdc).
Response — 200 OK (Paid)
{ "status": "paid", "value_coin": "117.59", "txid_out": "0xe85ed56174785b0bb9fcb522655f961675ad236f2aad2f5bb4fa2f074ac09726", "coin": "polygon_usdc" }
Response — 200 OK (Unpaid)
{ "status": "unpaid" }
Rate limiting This endpoint is for verification only — do not poll from the client. Recommended limits: max 1 request per 15 seconds per order for server-side verification, and a background job every 2–5 minutes for orders stuck in pending longer than 10 minutes. For client-facing status, use the Polling endpoint pattern.

Callback Verification

Callbacks arrive as unsigned GET requests. Anyone who knows your callback URL could send a fake request. Always verify before marking an order as paid.

Required Verification Flow

1

Receive Callback

Extract the order ID from your callback's query parameters (e.g. orderId). Look up the stored ipn_token for that order in your database.

2

Verify via Payment Status

Call GET /control/payment-status.php?ipn_token={stored_token} and confirm status=paid.

3

Validate Amount & TXID

Compare the returned value_coin against your expected order amount (with ±0.02 tolerance). Optionally verify txid_out matches.

4

Idempotency Check

Before marking paid, check that this txid_out hasn't already been processed (prevents replay attacks). Only then update your order status.

Pattern: Callback as trigger, Status endpoint as truth Use the callback to know when to check. Use the Payment Status endpoint to know if it's real.
Always return HTTP 200 immediately Your callback handler must respond with 200 OK within 5 seconds — even if the orderId is missing, the session is unknown, or verification fails. Return 200 first, then process asynchronously (e.g. enqueue a background job or cron task). For most integrations, inline verification completes well within 5 seconds — but for high volume, a queue is recommended. A slow or non-200 response causes retries and duplicate processing.

Callback Behavior

Understanding callback delivery behavior is critical for building a reliable integration.

Delivery Rules

BehaviorDetail
Method GET request to your callback URL with transaction data as query parameters.
Expected response Return HTTP 200 as quickly as possible. Response body is ignored. Do not return redirects (3xx).
Retries If your server returns a non-200 status or times out, the callback may be retried. Assume at-least-once delivery — your handler must be idempotent.
Timeout Your callback handler should respond within 5 seconds. If you need heavy processing (DB writes, email sending), return 200 first and process asynchronously.
Ordering Callbacks are not guaranteed to arrive in order. Always check current payment status rather than assuming sequence.
Duplicates You may receive the same callback multiple times. If the order is already paid, return 200 without re-processing.
What if the callback never arrives? Callbacks can fail due to network issues, server downtime, or DNS problems. Do not rely solely on callbacks. Implement a polling fallback for customer-facing confirmation, and run a background job that checks payment-status for orders stuck in pending longer than 10 minutes.

Idempotency

The API does not enforce idempotency — your integration must handle it on both Create Wallet and Callback.

Create Wallet

Each call generates a new wallet, even with identical parameters. If a network timeout triggers a retry, you'll end up with two wallets for one order. Always check your database first:

Pseudocode
// Before calling Create Wallet: const existing = await db.sessions.findUnique({ where: { orderId } }); if (existing) { // Wallet already exists for this order — reuse it return { redirect_url: buildCheckoutUrl(existing) }; } // No session exists — safe to create const wallet = await createWallet(merchantAddress, callbackUrl); await db.sessions.create({ orderId, ipnToken: wallet.ipn_token, status: "pending" });

Callback Handler

Protect against double-processing with an atomic conditional update and a unique txid_out constraint:

Pseudocode
// Already paid? Just acknowledge. if (session.status === "paid") return Response(200); // Atomic update: only if still pending await db.sessions.updateMany({ where: { orderId, status: "pending" }, data: { status: "paid", txidOut: status.txid_out, paidAt: new Date() } }); // txid_out column has UNIQUE constraint → prevents replay attacks

Payment Status Matrix

The upstream API returns paid or unpaid. Map these to richer internal states for better operational handling.

Internal StatusUpstreamConditionAction
pending unpaid Order created, awaiting payment Show "waiting" UI. Poll at intervals.
paid paid value_coin within ±0.02 of expected Mark order complete. Deliver goods/service.
paid_review paid value_coin deviates >0.02 from expected Payment received but amount mismatch. Flag for manual review.
expired unpaid Order older than timeout (e.g. 30 min) Show "session expired". Offer retry.
failed Upstream API error during verification Log error. Retry verification via background job.
Unknown statuses If the Payment Status endpoint returns an unexpected value, treat it as unpaid and flag for manual review. Do not auto-reject or auto-approve.

Error Handling

The API may return errors in the following format.

Error Response Format

Error Response
{ "status": "error", "message": "Invalid wallet address" }

Common HTTP Status Codes

CodeMeaningWhat to do
200 Success Parse response JSON normally.
400 Bad Request Check your parameters — likely a missing or malformed value (bad encoding, invalid wallet address).
422 Unprocessable Parameters are syntactically valid but semantically wrong (e.g. unsupported currency for provider).
429 Rate Limited Too many requests. Wait 30–60 seconds before retrying. Implement exponential backoff.
500 Server Error Transient failure. Retry with exponential backoff (max 3 retries).
Tip Always check for "status": "success" in the JSON response body, even on HTTP 200. Some errors may return 200 with an error message in the body.

Quickstart: Next.js (App Router)

A production-ready integration pattern using Next.js with proper callback verification, idempotency, and error handling.

1. Create Payment Session

app/api/payments/create-session/route.ts
import { NextResponse } from "next/server"; type CreateSessionBody = { orderId: string; amount: string; // always string, never float currency: string; // e.g. "EUR", "USD" provider: string; // e.g. "moonpay", "stripe" email?: string; }; export async function POST(req: Request) { const body = (await req.json()) as CreateSessionBody; // Idempotency: reuse existing session if one exists // const existing = await db.paymentSessions.findUnique({ // where: { orderId: body.orderId } // }); // if (existing) return NextResponse.json({ // status: "success", redirect_url: buildCheckoutUrl(existing) // }); // Build callback URL with your order identifier const callbackUrl = new URL( `${process.env.APP_URL}/api/payments/callback` ); callbackUrl.searchParams.set("orderId", body.orderId); // Step 1: Create Wallet const walletUrl = new URL( "https://pay.syncom-europe.com/control/wallet.php" ); walletUrl.searchParams.set("address", process.env.MERCHANT_USDC_WALLET!); walletUrl.searchParams.set("callback", callbackUrl.toString()); const walletRes = await fetch(walletUrl.toString()); if (!walletRes.ok) { return NextResponse.json( { status: "error", message: "wallet_create_failed" }, { status: 502 } ); } const wallet = await walletRes.json(); // Store ipn_token + expected amount in your DB // await db.paymentSessions.create({ // orderId: body.orderId, // ipnToken: wallet.ipn_token, // expectedAmount: body.amount, // currency: body.currency, // }); // Step 2: Build redirect URL for the customer const redirectUrl = new URL( "https://checkout.syncom-europe.com/process-payment.php" ); redirectUrl.searchParams.set("address", wallet.address_in); redirectUrl.searchParams.set("amount", body.amount); redirectUrl.searchParams.set("provider", body.provider); redirectUrl.searchParams.set("currency", body.currency); if (body.email) { redirectUrl.searchParams.set("email", body.email); } return NextResponse.json({ status: "success", redirect_url: redirectUrl.toString(), }); }

2. Callback Handler (with Verification)

app/api/payments/callback/route.ts
import { NextResponse } from "next/server"; export async function GET(req: Request) { const url = new URL(req.url); const orderId = url.searchParams.get("orderId"); const txidOut = url.searchParams.get("txid_out"); if (!orderId) { return NextResponse.json({ ok: true }); // Always 200 to prevent retries } // Load session from DB (replace with your ORM/DB call) const session = await db.paymentSessions.findUnique({ where: { orderId } }); if (!session) return NextResponse.json({ ok: true }); // unknown order — still 200 // Idempotency: already paid? Just acknowledge. if (session.status === "paid") { return NextResponse.json({ ok: true }); } // VERIFY: Never trust the callback — check payment status const statusUrl = new URL( "https://pay.syncom-europe.com/control/payment-status.php" ); statusUrl.searchParams.set("ipn_token", session.ipnToken); const statusRes = await fetch(statusUrl.toString()); if (!statusRes.ok) { return NextResponse.json({ ok: false }, { status: 502 }); } const status = await statusRes.json(); if (status.status === "paid") { // Verify amount (with tolerance for FX rounding) const paid = parseFloat(status.value_coin); const expected = parseFloat(session.expectedAmount); const newStatus = Math.abs(paid - expected) > 0.02 ? "paid_review" : "paid"; // Replay check: ensure txid_out not already used if (txidOut && status.txid_out && txidOut !== status.txid_out) { return NextResponse.json({ ok: false }, { status: 409 }); } // Mark order as paid + store txid_out (replace with your DB call) await db.orders.update({ where: { id: orderId, status: "pending" }, // atomic: only update if still pending data: { status: newStatus, txidOut: status.txid_out } }); } // Always return 200 to acknowledge the callback return NextResponse.json({ ok: true }); }
Pattern: Callback as trigger, Status endpoint as truth The callback tells you when to check. The Payment Status endpoint tells you whether it's real. Never update order status based on callback parameters alone.

Polling & Confirmation UX

After redirecting the customer to checkout, show a "Waiting for confirmation" page that polls your backend. The callback updates your DB; the polling endpoint reads from it.

Webhook-triggered + polling fallback The callback is the primary trigger for marking orders paid. Polling is the UX layer that lets the customer see the result in real-time. Your polling endpoint reads from your database — it does NOT call the upstream Payment Status API on every poll request.

Polling Endpoint

app/api/payments/status/route.ts
import { NextResponse } from "next/server"; const MIN_VERIFY_INTERVAL = 15_000; // 15s between upstream checks const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 min export async function GET(req: Request) { const orderId = new URL(req.url).searchParams.get("orderId"); if (!orderId) return NextResponse.json({ status: "error" }, { status: 400 }); // Read from DB — primary source of truth // const session = await db.paymentSessions.findUnique({ where: { orderId } }); // if (!session) return NextResponse.json({ status: "error" }, { status: 404 }); // Already complete? Return immediately. // if (["paid","paid_review"].includes(session.status)) { // return NextResponse.json({ // status: session.status, // redirect: `/order/${orderId}/success` // }); // } // Expired? // if (Date.now() - session.createdAt.getTime() > SESSION_TIMEOUT) { // return NextResponse.json({ status: "expired" }); // } // Throttled upstream fallback: only if callback hasn't arrived yet // and last check was > 15s ago // const sinceLastCheck = session.lastVerifiedAt // ? Date.now() - session.lastVerifiedAt.getTime() : Infinity; // if (sinceLastCheck > MIN_VERIFY_INTERVAL) { // const upstream = await checkUpstreamStatus(session.ipnToken); // await db.paymentSessions.update({ // where: { orderId }, // data: { lastVerifiedAt: new Date() } // }); // if (upstream?.status === "paid") { /* same verify logic */ } // } return NextResponse.json({ status: "pending" }); }

Client-side Adaptive Polling

JavaScript
async function pollPaymentStatus(orderId) { const start = Date.now(); const MAX_WAIT = 5 * 60 * 1000; // 5 min hard stop while (Date.now() - start < MAX_WAIT) { const elapsed = Date.now() - start; const interval = elapsed < 30_000 ? 2000 // 0-30s: every 2s : elapsed < 90_000 ? 5000 // 30-90s: every 5s : 10_000; // 90s+: every 10s try { const res = await fetch(`/api/payments/status?orderId=${orderId}`); const data = await res.json(); if (data.status === "paid" || data.status === "paid_review") { window.location.href = data.redirect || `/order/${orderId}/success`; return; } if (data.status === "expired") { showExpiredUI(); return; } } catch (err) { console.error("Poll error:", err); } await new Promise(r => setTimeout(r, interval)); } showTimeoutUI(); // "Couldn't confirm. Check email or contact support." }

Edge Cases

ScenarioHandling
User closes tab before confirmation Callback still arrives → order marked paid in DB. Send confirmation email as fallback.
Callback arrives late (1–3 min) Polling catches it via throttled upstream checks (lastVerifiedAt logic).
Callback never arrives Background cron job checks pending orders older than 10 min.
Multiple browser tabs polling lastVerifiedAt throttle prevents upstream API abuse (max 1 check per 15s per order).

Payment Providers

Use the provider parameter in Process Payment. Each provider has different minimum order amounts, supported currencies, and fee structures. Fees are generally lower for larger transaction amounts.

Provider IDCurrenciesCustomer KYCNotes
moonpayUSD, EUR, GBP + 30 moreRequired (most amounts)Wide global coverage. Popular default choice.
stripeUSD onlyMinimalUse Convert for EUR/GBP. Familiar checkout UX.
transakUSD, EUR, GBP + 50 moreAbove small amountsBest international currency coverage.
wertUSD onlyMinimal (small amounts)Fast customer onboarding. Use Convert for other currencies.
rampnetworkUSD onlyRequiredUse Convert endpoint. Strong EU regulatory compliance.
guardarianUSD, EUR + othersAbove limitsCompetitive fees.
mercuryoUSD, EUR, GBP + othersRequiredCompetitive fees on larger amounts.
banxaUSD, EUR, AUD + othersRequiredStrong in APAC region.
transfiUSD onlyRequiredUse Convert endpoint for other currencies.
utorgUSD, EUR + othersAbove limitsMulti-currency support.
revolutUSD, EUR, GBP + 30 moreRevolut accountRecommended for UK — bypasses 24h FCA cooling-off. Instant settlement.
Provider selection tips For USD-only providers, call the Convert to USD endpoint first. All disputes and chargebacks are handled by the provider — merchants are never involved. See Fee Handling for cost details.
Provider availability may change Supported currencies, KYC requirements, and provider availability can change without notice. The checkout UI always reflects the current state and is the source of truth. When in doubt, let customers choose their provider via the hosted multi-provider checkout.

Fee Handling

Understanding the fee structure helps you price correctly and set expectations with customers.

Fee Breakdown

FeePaid byAmountDetails
Provider fee Customer (at checkout) 3–7% Charged by MoonPay, Transak, etc. Shown transparently during their checkout. Lower for larger amounts.
Gateway fee Deducted from payout <1% Syncom Europe infrastructure fee. Reflected in the difference between value_coin and value_forwarded_coin.
Network fee Deducted from payout ~$0.01 Polygon USDC transfer fee. Negligible.

How Fees Work in Practice

Provider fees are always added on top of your order amount at the provider's checkout. The customer sees a transparent breakdown:

Customer sees at provider checkout
Order amount: $100.00 Provider fee: +$4.50 ───────────────────────── Customer pays: $104.50 → You receive: ~$99.00 USDC (after gateway + network fees)

Provider fees are typically charged to the customer at the provider's checkout page. The merchant specifies the order amount, and the provider adds their fee on top. The final received amount may differ slightly due to FX rounding — always use value_forwarded_coin from the callback for accounting, not the original order amount.

Low-cost, no merchant contract required No monthly fees, no setup fees, no chargeback exposure. Gateway fees under 1%, provider fees shown to customer at checkout. Significantly lower cost compared to traditional payment processors.

Custom Checkout Domain

By default, your customers see checkout.syncom-europe.com during payment. If you want your own branded checkout domain (e.g. checkout.yourcompany.com), follow this guide.

How It Works

You create a Cloudflare Worker on your own domain that proxies requests to pay.syncom-europe.com and checkout.syncom-europe.com. Your customers see your domain throughout the entire payment flow, while all processing (including affiliate commission) stays intact.

1

Add your domain to Cloudflare

Create a free Cloudflare account and add your domain. Change your nameservers to the ones provided by Cloudflare.

2

Create DNS records

Add proxied (orange cloud) A records for your subdomains. The IP can be anything (e.g. 8.8.8.8) — it will be overridden by the Worker route.

3

Create a Cloudflare Worker

Go to Workers & Pages → Create → Worker. Paste the worker code below and deploy.

4

Add routes

In the Worker settings, add routes for both subdomains with a wildcard: pay.yourcompany.com/* and checkout.yourcompany.com/*

Worker Code

Replace pay.yourcompany.com and checkout.yourcompany.com with your actual subdomains:

worker.js — Custom Domain Proxy
export default { async fetch(request) { const url = new URL(request.url); // ── CONFIG: replace with your domains ── const MY_API = "pay.yourcompany.com"; const MY_CHECKOUT = "checkout.yourcompany.com"; // Upstream: your domains → Syncom Europe const UPSTREAM_API = "pay.syncom-europe.com"; const UPSTREAM_CHECKOUT = "checkout.syncom-europe.com"; const host = url.hostname.toLowerCase(); const isCheckout = host === MY_CHECKOUT; const upstreamHost = isCheckout ? UPSTREAM_CHECKOUT : UPSTREAM_API; // Build upstream URL const upstream = new URL(url.pathname + url.search, `https://${upstreamHost}`); // Forward request const headers = new Headers(request.headers); headers.delete("host"); const resp = await fetch(upstream, { method: request.method, headers, body: request.method === "GET" ? undefined : request.body, redirect: "manual", }); // Rewrite Location headers const outHeaders = new Headers(resp.headers); const loc = outHeaders.get("location"); if (loc) { outHeaders.set("location", loc .replaceAll(UPSTREAM_CHECKOUT, MY_CHECKOUT) .replaceAll(UPSTREAM_API, MY_API) ); } const origin = request.headers.get("origin"); outHeaders.set("Access-Control-Allow-Origin", origin || "*"); outHeaders.set("Vary", "Origin"); // Rewrite text responses const ct = outHeaders.get("content-type") || ""; if (ct.includes("json") || ct.includes("text/") || ct.includes("html")) { const body = (await resp.text()) .replaceAll(UPSTREAM_CHECKOUT, MY_CHECKOUT) .replaceAll(UPSTREAM_API, MY_API); return new Response(body, { status: resp.status, headers: outHeaders }); } return new Response(resp.body, { status: resp.status, headers: outHeaders }); }, };

DNS Records Example

TypeNameContentProxy
A pay 8.8.8.8 Proxied (orange cloud)
A checkout 8.8.8.8 Proxied (orange cloud)

After Setup

Once deployed, use your custom domains in all API calls and redirects:

Your custom integration
// Create Wallet — uses your API domain const walletUrl = new URL("https://pay.yourcompany.com/control/wallet.php"); walletUrl.searchParams.set("address", "0xYOUR_USDC_WALLET"); walletUrl.searchParams.set("callback", callbackUrl); // Redirect — uses your checkout domain const checkoutUrl = new URL("https://checkout.yourcompany.com/process-payment.php"); // ... same parameters as documented
API contract is identical The entire API works exactly the same through your custom domain. The only difference is the hostname — all endpoints, parameters, callbacks, and responses behave identically.
Free tier is sufficient The free Cloudflare plan supports 100,000 Worker requests per day — more than enough for most integrations. No paid plan required.
CORS note The worker sets Access-Control-Allow-Origin dynamically from the request origin. If you need to send credentials (cookies/auth headers) from the browser, set an explicit origin (not *) and add Access-Control-Allow-Credentials: true.

Integration Checklist

Verify these items before going live.

#ItemWhy
1 ipn_token stored server-side only It's a secret — never expose in frontend, URLs, or logs.
2 Callback returns 200 within 5 seconds Non-200 or slow responses trigger retries and duplicate processing.
3 Always verify via Payment Status endpoint Callback query params can be spoofed — never trust for settlement.
4 txid_out stored with UNIQUE constraint Prevents replay attacks and double-crediting.
5 Polling endpoint reads from DB, not upstream Avoids rate limiting and reduces upstream API load.
6 Background job for stuck pending orders (>10 min) Catches cases where callback never arrives.

Requirements

For the Merchant

A USDC wallet on the Polygon network. Use any self-custodial wallet — Trust Wallet (SWIFT option), MetaMask, Coinbase Wallet, or similar.

For the Customer

A 3D Secure enabled card (Verified by Visa / Mastercard SecureCode). First-time customers may need to complete identity verification (KYC) with the payment provider — typically under 2 minutes.

Technical Requirements

Callback endpoints must use HTTPS. The callback handler must respond within a reasonable timeout. Store ipn_token values in your database for payment verification.

Payment finality USDC payouts are irreversible on-chain. Card disputes are handled entirely by the payment provider — merchants receive USDC payouts and are not involved in chargeback processes.

Rate Limits & Fair Use

There are no hard rate limits, but we expect reasonable use. Do not poll the Payment Status endpoint more than once per 15 seconds per order. Excessive automated requests may be throttled without notice.

Support

For integration questions and technical support, contact support@syncom-europe.com.

Changelog

VersionDateChanges
v1.1 2025-02-13 Added: Callback Behavior (retry/timeout rules), Idempotency guidance, Payment Status Matrix, Polling & Confirmation UX, Fee Handling section, Custom Checkout Domain guide, enhanced Provider table with KYC info. Fixed: URL encoding section (URLSearchParams recommended), Process Payment classified as browser redirect, currency parameter HTML encoding.
v1.0 2025-02-13 Initial release — Create Wallet, Process Payment, Convert to USD, Callback Event, Payment Status.