Sending Events
The SDK records what contacts do in your app as events on their timeline, which you can use in segments and automations. How events are stored and which names and attributes work well: Events.
Requirements
- The SDK installed and initialized, see Installation.
- A successful
ping()in this installation. Events need the contact key that the first ping returns. See Contacts.
Event types
| Type | Method | When to use |
|---|---|---|
| Custom events | sendEvent(name:attributes:_:) | Anything specific to your app: a feature used, a form submitted, a purchase step |
| Screen events | trackScreen(name:_:) | A screen the contact opened |
| Product events | sendProductEvent(_:eventType:attributes:_:) | Commerce actions on a product: view, add to cart, purchase, refund and more |
The names before 1.3.0, sendEvent(with:params:_:), trackScreen(with:_:) and sendProductEvent(_:eventType:name:productURL:params:_:), still work and are deprecated until 2.0. See the Migration Guide.
Setup
- Make sure
ping()runs at app launch, before the first event. - Pick stable event names and a fixed set of attribute names. See Naming rules.
- Send events with the method for the event type.
UserSDK.default?.sendEvent(name: "subscription_started", attributes: ["plan": "pro"]) { success, error in
if !success { print("Event failed: \(String(describing: error))") }
}
The event appears on the contact's timeline in the panel, and the completion reports true.
Handling
Results
Every event method accepts an optional completion handler (Bool, Error?) -> Void:
| Result | Meaning |
|---|---|
true, nil | The backend accepted the event. |
false, notIdentified | No contact yet. The event was not queued. Call ping() first. |
false, invalidName or invalidData | The name or an attribute value cannot be sent. Nothing was sent. |
false, rejected(statusCode:) | The backend rejected the event: a 4xx at once, a 5xx or 429 after all retries. |
false, network | No connection after all retries. See Delivery and Retries. |
The handler is not called for requests that are still waiting in the queue when the app closes. They are sent after the next launch.
Completion thread
Every completion runs on the main thread, and never before the call returns. This includes errors the SDK detects before any request. You can update the UI directly in the completion:
UserSDK.default?.sendEvent(name: "subscription_started", attributes: ["plan": "pro"]) { success, error in
// update the UI
}
Before 1.3.0 completions could run on a background thread, except the completion of logout. Code that switches to the main queue keeps working.
A rejected request (for example HTTP 400 or 401) calls completion(false, error). Before 1.1.0 every event reported success regardless of the response. If your code treats true as proof of delivery, review it.
Errors
Every failure reaches your completion as a UserSDKError:
| Error | Meaning | Request sent |
|---|---|---|
notIdentified | No contact yet, call ping() first. | No |
invalidName | The event name, screen name or product ID is not accepted. | No |
invalidData | An attribute value JSON cannot hold, or an empty attribute name. | No |
rejected(statusCode:) | The server refused the request. | Yes |
network | No connection after all retries. | Yes or no |
notCollectingData | A ping was answered without a contact key: the app does not collect data. Check the panel. | Yes |
invalidConfiguration | The request could not be built. Check the base URL passed to the SDK. | No |
loggedOut | The request was dropped because the user logged out before it was sent. | No |
UserSDK.errorHandler receives every error on the main thread, after the completion of the call. It also receives errors from calls without a completion, from push and in-app message reports, and a click from an email link that could not be reported. Use it to log SDK errors in one place:
UserSDK.errorHandler = { error in
Crashlytics.crashlytics().record(error: error)
}
A request waiting offline in the queue is not an error: its result comes when it is sent or finally dropped.
In Objective-C the error is an NSError with the domain com.user.sdk and the codes 1 to 8 in the order of the table above. For rejected, userInfo holds the HTTP status under statusCode. errorHandler is available in Swift only.
Naming rules
- Use the same spelling and letter case for an event name every time.
Purchaseandpurchaseare treated as the same name, and the second spelling is rejected by the backend: the completion reportsrejected(statusCode: 500)after all retries. - An event name must not be empty, longer than 255 characters or contain HTML. A screen name is at most 128 characters. The SDK checks this and reports
invalidNamewithout a request. - Use a fixed set of attribute names with stable value types. Every new attribute name creates a new event attribute, and its first value sets its type for good.
- Do not send
nilvalues or empty attribute names.
Attribute value types
Attribute values must be JSON values:
| Type | Example |
|---|---|
String | "checkout" |
Int | 3 |
Double | 49.99 |
Bool | true |
| Array | ["red", "blue"] |
| Dictionary | ["size": "42"] |
| Date as ISO 8601 string | "2026-09-28T12:51:33Z" |
Any other value, for example Date, URL, Data or a Double that is NaN, ends with invalidData and nothing is sent. Before 1.3.0 such values crashed the app. Send dates as ISO 8601 strings.
The SDK adds the contact key and the time of the event to every request. Events created offline keep the time they were created.
Common errors
| Symptom | Cause | Fix |
|---|---|---|
Completion reports notIdentified ("No user exists yet") | The event was sent before the first successful ping. | Call ping() at launch and send events after it succeeds. |
Completion reports invalidName or invalidData | The name breaks the naming rules, or a value is not a JSON value. | Fix the name or the value. |
| Events are not on the contact you expect | The device is tied to a different contact than the one you look at. | Compare UserSDK.default?.userKey with the contact key in the panel. See Troubleshooting. |
| The event is recorded, but some attributes are missing | The attribute value has a different type than the first value ever sent for it, or the name is invalid. | Keep attribute types stable. |
| An event name never appears in the panel | The same name was used before with different letter case. | Use one exact spelling. |
| No screen events at all | Screens are not tracked automatically. | Call trackScreen(name:_:) or use the Trackable view controllers. |