Customers
A customer (an “external user”) is an end user of your application, identified by your stable ID. Customers are separate from dashboard users, who manage projects. The userId is the key that ties everything together: the profile, the wallet, the events your app sends, referral records and workflow history.
The userId is the identity
Section titled “The userId is the identity”- You choose the
userId(e.g. your database primary keycus_8f2k). Storelayer never generates or changes it. - IDs are strings compared exactly:
"123"and" 123"are different customers. - Wallets, events and referrals use the
userIdwithout checking that a profile exists. A profile is optional for a customer to earn points, but it is what rules read for personalised conditions (name, birthday, metadata).
Use the same userId everywhere. Do not swap in an email or phone number. If a customer changes their email, update the profile field; the ID stays.
Profile fields
Section titled “Profile fields”| Field | Notes |
|---|---|
userId | Required, your ID. Returned as id |
firstName, lastName | Required when registering. Stored as empty strings when a profile is created by upsert without them |
email | Optional, must be a valid email |
phone, birthday, gender | Optional free-form strings. Use ISO dates (1990-04-21) for birthday so conditions can compare them |
metadata | Optional object for your own attributes (tier, signupSource, marketingOptIn, …) |
Storelayer also adds projectId, createdAt and updatedAt.
Metadata merges
Section titled “Metadata merges”Updating metadata merges the keys you send into the existing object: other keys are kept. To delete a key, send it with value null. Over the REST API, metadata comes back as a JSON-encoded string, so parse it. Inside rules it is already an object: {{ $('customer').metadata.tier }}.
Create vs upsert
Section titled “Create vs upsert”| You want | Use | Behavior for an existing userId | Behavior for an unknown userId |
|---|---|---|---|
| Create a profile exactly once (signup) | POST /projects/{projectId}/users | 409, nothing changes | Created (201) |
| Keep a profile in sync from your system | PATCH /projects/{projectId}/users/{userId} | Fields you send are updated (200) | Created (201) |
| Import many profiles | POST /projects/{projectId}/users/bulk | Whole batch rejected (409) | All created |
| Check whether a customer exists | GET /projects/{projectId}/users/{userId} | 200 with the profile | 404 |
PATCH creates customers. Never use it to check whether someone exists: a typo creates a new, empty profile.
Deleting a customer
Section titled “Deleting a customer”DELETE /projects/{projectId}/users/{userId} removes only the profile. The customer’s wallet balances and ledger, referral codes and referral records, and workflow history remain under the same userId. Re-registering the same ID reconnects the new profile to that existing data. If you need everything gone, zero the wallet with a modify to 0 per asset type, deactivate the referral code, and keep your own deletion record.
Customers in rules
Section titled “Customers in rules”Rules read profiles through an internal resource, e.g. external_users.get_user with userId taken from the event. If the profile does not exist, the resource resolves to an object with an _error field instead of failing the rule, so exists/notExists checks on profile fields behave predictably. Rules can also write profiles with the update_user action (same merge rules as PATCH). See conditions and resources.
See also
Section titled “See also”- Working with customer data: request examples, search, bulk import
- Wallets
- Authentication