time2pay Developer Docs

Guide

Go-live checklist

What to verify before real money flows, and a smoke test that proves your setup in one call.

Before real money

Credentials

  • The API secret lives in a secret store, on backend hosts only — never in a repository, a mobile app or a page.
  • Your sign matches the test vector.
  • Every host that calls us keeps time with NTP. Requests more than 5 minutes off are refused.
  • If you use an IP allowlist, it names every egress address, workers included.
  • The smoke test passes from each of those hosts.

Deposits

  • Every create sends a unique external_order_id, and a retry reuses it.
  • You credit on the webhook, never on a redirect, an iframe event or a processing step.
  • Wallet-address deposits credit credited, not amount — see Wallet.
  • Your cashier shows the network next to every address, so a player does not send BEP-20 USDT to a Tron address.

Webhooks

  • The callback URL is HTTPS and reachable from the internet. It is set in the admin panel, or per request with callback_url.
  • Your handler verifies the signature on the raw body, and rejects the webhook test vector with a single byte changed.
  • It deduplicates on X-Idempotency-Key, answers 2xx within a few seconds and processes afterwards.
  • You reconcile anything still open after an hour with the status endpoints, in case your endpoint was down through all eight attempts.

Payouts

  • A payout wallet is configured for each family you pay out on — EVM and Tron are separate addresses.
  • Its private key is held where only the signing service can use it: an HSM, a KMS or a hardware wallet, not an environment variable on a web server.
  • It holds the tokens, and the routers have an allowance — the single router and the batch router separately.
  • Before signing, your code compares every recipient and amount in typed_data with your own withdrawal records.
  • You handle insufficient_allowance and insufficient_funds — see handling errors.
  • You send a unique external_id per withdrawal, and move a withdrawal to paid only on payout.completed.

First real transactions

  • One small deposit on each network and each flow you use, followed through to the credited ledger entry.
  • One small payout and one two-item batch on each network, followed through to payout.completed.
  • One deliberately failed case: a payout above your allowance, to see your handling end to end.

Smoke test

One read-only signed call. If it succeeds, the key, the secret, the signature code, the clock and the IP allowlist are all right; if it fails, the error code says which. It moves nothing, so run it from every host and in your deploy pipeline.

func main() {
	client := &time2pay.Client{BaseURL: os.Getenv("BASE_URL"), KeyID: os.Getenv("API_KEY_ID"), Secret: os.Getenv("API_KEY_SECRET")}
	res, err := client.Call(context.Background(), "GET", "/v1/balance", "")
	if err != nil {
		log.Fatalf("not ready: %v", err)
	}
	log.Printf("key, signature, clock and IP allowlist all fine; settlement wallet %v", res["wallet"])
}
<?php

require __DIR__ . '/Time2payClient.php';

$client = new Time2payClient(getenv('BASE_URL'), getenv('API_KEY_ID'), getenv('API_KEY_SECRET'));
try {
    $res = $client->call('GET', '/v1/balance');
    echo "key, signature, clock and IP allowlist all fine; settlement wallet {$res['wallet']}\n";
} catch (Throwable $e) {
    fwrite(STDERR, "not ready: {$e->getMessage()}\n");
    exit(1);
}
import { createClient } from "./time2pay.js";

const client = createClient({
  baseUrl: Deno.env.get("BASE_URL"),
  keyId: Deno.env.get("API_KEY_ID"),
  secret: Deno.env.get("API_KEY_SECRET"),
});
const res = await client.call("GET", "/v1/balance");
console.log(`key, signature, clock and IP allowlist all fine; settlement wallet ${res.wallet}`);
import os
import sys

from time2pay import Time2payClient

client = Time2payClient(os.environ["BASE_URL"], os.environ["API_KEY_ID"], os.environ["API_KEY_SECRET"])
try:
    res = client.call("GET", "/v1/balance")
except Exception as e:
    sys.exit(f"not ready: {e}")
print(f"key, signature, clock and IP allowlist all fine; settlement wallet {res['wallet']}")
import { Time2payClient } from "./time2pay.mjs";

const client = new Time2payClient({
  baseUrl: process.env.BASE_URL,
  keyId: process.env.API_KEY_ID,
  secret: process.env.API_KEY_SECRET,
});
const res = await client.call("GET", "/v1/balance");
console.log(`key, signature, clock and IP allowlist all fine; settlement wallet ${res.wallet}`);
ResultMeaning
200Ready.
400 validation, "no settlement wallet configured"Key, signature, clock and allowlist are all fine — the request got past every check. Your merchant simply has no EVM settlement wallet, which is normal if you only use Tron. Count it as a pass.
401 unauthorizedWrong key id or secret, a signing bug (check the test vector), or a clock more than 5 minutes off.
403 forbiddenThis host's IP is not in your allowlist.
503 unavailableYour key is fine; we cannot read the chain right now. Retry.