Skip to main content

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​

TypeMethodWhen to use
Custom eventssendEvent(name:attributes:_:)Anything specific to your app: a feature used, a form submitted, a purchase step
Screen eventstrackScreen(name:_:)A screen the contact opened
Product eventssendProductEvent(_: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​

  1. Make sure ping() runs at app launch, before the first event.
  2. Pick stable event names and a fixed set of attribute names. See Naming rules.
  3. 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))") }
}
Check

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:

ResultMeaning
true, nilThe backend accepted the event.
false, notIdentifiedNo contact yet. The event was not queued. Call ping() first.
false, invalidName or invalidDataThe 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, networkNo 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
}
Changed in 1.3.0

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.

Changed in 1.1.0

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:

ErrorMeaningRequest sent
notIdentifiedNo contact yet, call ping() first.No
invalidNameThe event name, screen name or product ID is not accepted.No
invalidDataAn attribute value JSON cannot hold, or an empty attribute name.No
rejected(statusCode:)The server refused the request.Yes
networkNo connection after all retries.Yes or no
notCollectingDataA ping was answered without a contact key: the app does not collect data. Check the panel.Yes
invalidConfigurationThe request could not be built. Check the base URL passed to the SDK.No
loggedOutThe 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. Purchase and purchase are treated as the same name, and the second spelling is rejected by the backend: the completion reports rejected(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 invalidName without 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 nil values or empty attribute names.

Attribute value types​

Attribute values must be JSON values:

TypeExample
String"checkout"
Int3
Double49.99
Booltrue
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​

SymptomCauseFix
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 invalidDataThe 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 expectThe 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 missingThe 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 panelThe same name was used before with different letter case.Use one exact spelling.
No screen events at allScreens are not tracked automatically.Call trackScreen(name:_:) or use the Trackable view controllers.