Obtain an access token
The first stage of the two-stage process. The only endpoint that does not require x-auth-token, and the only one without a scope requirement.
Three differences from common implementations that must be handled correctly:
- Multiple tokens may coexist. Issuing a new token does not invalidate old ones; integrations with multiple processes need not worry about workers invalidating each other's tokens. Cache and reuse tokens until shortly before expiry rather than obtaining a new one for each call.
- Issuance is limited to 20 requests/minute/client_id; exceeding it returns
rate_limited. Frequent token issuance is itself a sign of an integration problem. - Token
scopesare informational only. Authorization reads the current Key row for every request; administrative scope changes and Key disabling therefore take effect immediately, without waiting for token expiry.
The returned scopes are expanded: enabling members:write also includes members:read. Expansion happens during authorization rather than when stored, so later rule tightening also applies to existing Keys.
Cross-environment credentials are always rejected, with environment_mismatch rather than Invalid credentials. Comparable upstream APIs return Invalid client id here, which integrators often misdiagnose as a copied-key error. A sandbox Key resolves to a shadow merchant entity, with data separate from live: members and ledger entries created in the sandbox do not exist in live.
Key rotation has an overlap window: the old key can still obtain tokens before prev_valid_until, allowing a gradual transition. ⚠ This is a hard cutoff, not a reminder: it becomes invalid immediately at that time, without a grace period.
Prerequisites
- The API Key is active and has not passed
expires_at. - The Key's environment matches the hostname being called (
api-sandbox.*for sandbox Keys). - Live Keys must have an IP allowlist, and the current outbound IP must match it.
Request Headers
| Field | Type | Required | Description |
|---|---|---|---|
x-client-id |
string | Required | The API Key's client_id, issued when creating the Key in the merchant portal. |
x-api-key |
string | Required | The API Key secret. ⚠ This is not the signing key. A Key provides two values:
api_key is used only here and stored by us only as a hash; signing_key is used for
x-signature. Confusing them causes every signature check to fail. |
Response
{
"auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expired_at": 1786000800,
"token_type": "Bearer",
"scopes": [
{
"members": "read"
},
{
"members": "write"
},
{
"merchant": "read"
}
]
}environment_mismatch: sandbox credentials used on a live host, or vice versa.invalid_credentials: the same response for five cases: missing headers,
nonexistent client_id, disabled Key, expired Key, or incorrect secret.
Distinguishing them would enable client_id probing.rate_limited: 20 requests per client_id per minute.curl -X POST 'https://api.zinfra.vip/v1/connect/token' \
-H 'x-auth-token: Bearer $TOKEN'const res = await fetch("https://api.zinfra.vip/v1/connect/token", {
method: "POST",
headers: {
"x-auth-token": "Bearer $TOKEN",
},
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();import requests
res = requests.post(
"https://api.zinfra.vip/v1/connect/token",
headers={
"x-auth-token": "Bearer $TOKEN",
},
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/connect/token",
nil)
req.Header.Set("x-auth-token", "Bearer $TOKEN")
res, err := http.DefaultClient.Do(req)
// Decode amount fields as string, not float64.HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.zinfra.vip/v1/connect/token"))
.header("x-auth-token", "Bearer $TOKEN")
.method("POST", HttpRequest.BodyPublishers.noBody())
.build();
// Use String / BigDecimal for amounts, not double.$ch = curl_init('https://api.zinfra.vip/v1/connect/token');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-auth-token: Bearer $TOKEN',
],
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
{
"auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expired_at": 1786000800,
"token_type": "Bearer",
"scopes": [
{
"members": "read"
},
{
"members": "write"
},
{
"merchant": "read"
}
]
}