Working with Customer Data
Customer endpoints live under /projects/{projectId}/users. Responses use the { "success": true, "data": … } envelope. The model and the create-vs-upsert decision are covered in customer concepts.
Register at signup
Section titled “Register at signup”POST /projects/{projectId}/users
{ "userId": "cus_8f2k", "firstName": "Alice", "lastName": "Smith", "phone": "+905551112233", "birthday": "1990-04-21", "metadata": { "tier": "silver", "signupSource": "ios" }}userId, firstName and lastName (non-empty) are required. Returns 201 with the profile. If the userId already exists, you get 409 with error.code 2500 and the profile is unchanged. Treat that as “already registered”, not as a failure to retry.
Upsert from your system of record
Section titled “Upsert from your system of record”PATCH /projects/{projectId}/users/{userId}
{ "metadata": { "tier": "gold", "signupSource": null }}Only the fields you send change. metadata is merged: tier is set, signupSource is removed, and every other key stays. Returns 200 when the profile was updated and 201 when it was created because the userId was unknown. Names are sent as non-empty strings or left out; to clear a top-level field such as email, there is no null option over REST.
Bulk import
Section titled “Bulk import”POST /projects/{projectId}/users/bulk
{ "users": [ { "userId": "cus_002", "firstName": "Alan", "lastName": "Turing" } ]}1–1000 users per request, each in the same shape as register. The batch is all-or-nothing: if any userId appears twice in the request or already exists, the whole request fails with 409 2501 and nothing is created. On success it returns { "created": n, "failed": 0 }. For re-runnable imports of data that may already exist, split into batches and, on 409, fall back to PATCH per user.
Look up and search
Section titled “Look up and search”| Need | Request |
|---|---|
| Exact customer by ID | GET /projects/{projectId}/users/{userId}; 404 2502 if missing |
| Find by email, phone or name | GET /projects/{projectId}/[email protected] |
| Page through all customers | GET /projects/{projectId}/users?limit=100&offset=200 |
search is a case-insensitive substring match across id, firstName, lastName, the full name, email and phone (max 200 characters). It can return several customers, for example search=smith. Confirm an exact field match before acting on a search result, especially before wallet writes. Lists return { "items": [...], "total": n } with the newest first; limit is 1–100 (default 50).
metadata comes back as a JSON-encoded string, e.g. "{\"tier\":\"gold\"}". Parse it before reading keys.
A customer’s activity
Section titled “A customer’s activity”GET /projects/{projectId}/users/{userId}/events?limit=20 lists the workflows started by events sent with this userId, newest first. Use it to see whether a purchase event was processed and which rules ran. See workflows.
Delete
Section titled “Delete”DELETE /projects/{projectId}/users/{userId} returns { "deleted": true }, also when the profile did not exist. It removes only the profile; the wallet, referral data and workflow history stay. See customer concepts.
Errors
Section titled “Errors”| HTTP | error.code | When |
|---|---|---|
| 409 | 2500 | Register: the userId already exists |
| 409 | 2501 | Bulk: duplicate IDs in the request, or some IDs already exist |
| 404 | 2502 | Get: no profile for this userId |
| 400 | — | Validation: missing names on register, invalid email, more than 1000 users |
Verify
Section titled “Verify”After a write, GET /projects/{projectId}/users/{userId} and compare each field you sent; check that other metadata keys were kept. After a bulk import, page through the list with search on a few IDs, or compare total before and after.
See also
Section titled “See also”- Customer concepts
- Working with wallets
- External Users in the API reference