Create a member
x-idempotency-key
Two idempotency layers: external_member_id naturally deduplicates; resubmitting the same external ID returns the existing row with 200 rather than 201. x-idempotency-key provides an additional layer. Already exists is therefore not an error; branch on the status code.
⚠ If the email already belongs to another merchant's member on the platform, this returns 400 invalid_request and does not create a member. Identity separation, moving email uniqueness from users to identities, has not yet been implemented. The global unique index remains, so the same individual cannot currently create separate accounts under two merchants.
This is not an Already registered response: it is identical to the generic Invalid request response, with nothing indicating that the email exists elsewhere. Distinguishing the cases would enable cross-merchant member probing. Consequently, after 400 you cannot determine yourself whether the cause is bad format or this restriction; ask the user to use another email or contact us.
If the email already belongs to your own merchant, we recognize the existing member, attach your external_member_id, and return 200.
The account receives a random placeholder password and does not use our password login. Members enter through your App or our hosted screens.
Emits member.created.
Prerequisites
- The email does not belong to another merchant's member on the platform.
Request Headers
| Field | Type | Required | Description |
|---|---|---|---|
x-idempotency-key |
string | Required | UUID v4 |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
external_member_id |
string | Required | Your own member identifier, maximum 120 characters. It is the natural
idempotency key for this business line and one of the accepted values for all subsequent x-on-behalf-of headers. |
email |
string | Required | Member email. The server stores it in lowercase and validates only that it contains @. It is also the identity layer's merging key, as described above. |
Response
external_member_id already exists; returns the existing row without creating a new member.{
"id": "mem_9f1c0c8e-6f2a-4c1d-9d0b-2a7e5b3f8c41",
"external_member_id": "u-10086",
"uid": "80031427",
"email": "alice@example.com",
"nickname": "",
"status": "normal",
"kyc_level": 0,
"created_at": "2026-08-12T09:30:00.000Z"
}invalid_request: the same response for three cases: body is not JSON;
external_member_id is empty or exceeds 120 characters, or email is empty or lacks @;
the email already belongs to another merchant's member, as described above.insufficient_scope: missing members:write.idempotency_key_reused · idempotency_in_progressEmitted Events
Green = successful terminal state · Red = terminal state requiring action · Purple = intermediate state. Open an event for its payload and signature verification details.
curl -X POST 'https://api.zinfra.vip/v1/members' \
-H 'x-auth-token: Bearer $TOKEN' \
-H 'x-idempotency-key: $IDEMPOTENCY_KEY' \
-H 'content-type: application/json' \
-d '{
"external_member_id": "u-10086",
"email": "alice@example.com"
}'const res = await fetch("https://api.zinfra.vip/v1/members", {
method: "POST",
headers: {
"x-auth-token": "Bearer $TOKEN",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
body: JSON.stringify({
"external_member_id": "u-10086",
"email": "alice@example.com"
}),
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();import requests
res = requests.post(
"https://api.zinfra.vip/v1/members",
headers={
"x-auth-token": "Bearer $TOKEN",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
json={
"external_member_id": "u-10086",
"email": "alice@example.com"
},
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/members",
strings.NewReader(`{
"external_member_id": "u-10086",
"email": "alice@example.com"
}`))
req.Header.Set("x-auth-token", "Bearer $TOKEN")
req.Header.Set("x-idempotency-key", "$IDEMPOTENCY_KEY")
req.Header.Set("content-type", "application/json")
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/members"))
.header("x-auth-token", "Bearer $TOKEN")
.header("x-idempotency-key", "$IDEMPOTENCY_KEY")
.header("content-type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"external_member_id": "u-10086",
"email": "alice@example.com"
}
"""))
.build();
// Use String / BigDecimal for amounts, not double.$ch = curl_init('https://api.zinfra.vip/v1/members');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-auth-token: Bearer $TOKEN',
'x-idempotency-key: $IDEMPOTENCY_KEY',
'content-type: application/json',
],
CURLOPT_POSTFIELDS => <<<'JSON'
{
"external_member_id": "u-10086",
"email": "alice@example.com"
}
JSON,
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
// No response example is declared in the specification for this operation.