Skip to main content

Send your first product event

One HTTP request tells PostDeploy what a person did in your application. From those events you get funnels, activation and retention, with no SDK to install.

Samples run on , facts checked on

Create a write key

  1. Step 1. Ask your agent to create a product analytics source for this project.

    Prompt text
    Create a product analytics source named iOS app for the production environment. Show me the write key.
  2. Step 2. Copy the key from the response. It starts with pda_ and is shown once. It can write events into this one project and nothing else, so you can ship it inside a mobile or browser application.

Send one event

Send the key as a bearer token. Only event is required.

Terminal bash
curl https://ingest.postdeploy.dev/v1/events \
  -H "Authorization: Bearer pda_YOUR_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "onboarding_completed",
    "distinct_id": "anon_123",
    "properties": {
      "platform": "ios",
      "app_version": "1.4.0",
      "variant": "onboarding_b"
    }
  }'

PostDeploy answers 202 with {"success": true, "accepted": 1}. Open Analytics, then Live events, to watch it arrive.

Send a batch

Batch up to 100 events in one request. Send the whole batch or none of it: one invalid event refuses the request, so you always know what still needs sending.

Terminal bash
curl https://ingest.postdeploy.dev/v1/events/batch \
  -H "Authorization: Bearer pda_YOUR_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      { "event": "onboarding_started", "distinct_id": "anon_123" },
      { "event": "onboarding_completed", "distinct_id": "anon_123" }
    ]
  }'

JavaScript

Never block a person's interaction on analytics. Send it and move on.

analytics.js javascript
function capture(event, distinctId, properties) {
  fetch("https://ingest.postdeploy.dev/v1/events", {
    method: "POST",
    headers: {
      "Authorization": "Bearer " + PD_WRITE_KEY,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ event, distinct_id: distinctId, properties }),
    keepalive: true
  }).catch(() => {})
}

capture("paywall_viewed", anonId, { source: "onboarding" })

Swift

Analytics.swift swift
func capture(_ event: String, distinctId: String, properties: [String: Any] = [:]) {
    var request = URLRequest(url: URL(string: "https://ingest.postdeploy.dev/v1/events")!)
    request.httpMethod = "POST"
    request.setValue("Bearer \(writeKey)", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try? JSONSerialization.data(withJSONObject: [
        "event": event,
        "distinct_id": distinctId,
        "properties": properties
    ])

    URLSession.shared.dataTask(with: request).resume()
}

Task { capture("meal_logged", distinctId: anonId, properties: ["source": "camera"]) }

Kotlin

Analytics.kt kotlin
suspend fun capture(event: String, distinctId: String, properties: Map<String, Any> = emptyMap()) {
    val payload = JSONObject(mapOf(
        "event" to event,
        "distinct_id" to distinctId,
        "properties" to JSONObject(properties)
    )).toString()

    val request = Request.Builder()
        .url("https://ingest.postdeploy.dev/v1/events")
        .header("Authorization", "Bearer $writeKey")
        .post(payload.toRequestBody("application/json".toMediaType()))
        .build()

    runCatching { client.newCall(request).execute().close() }
}

Ruby

analytics.rb ruby
def capture(event, distinct_id, properties = {})
  uri = URI("https://ingest.postdeploy.dev/v1/events")
  request = Net::HTTP::Post.new(uri)
  request["Authorization"] = "Bearer #{ENV.fetch("PD_WRITE_KEY")}"
  request["Content-Type"] = "application/json"
  request.body = JSON.generate(event: event, distinct_id: distinct_id, properties: properties)

  Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
rescue StandardError
  nil
end

Elixir

analytics.ex elixir
def capture(event, distinct_id, properties \\ %{}) do
  Task.start(fn ->
    Req.post("https://ingest.postdeploy.dev/v1/events",
      auth: {:bearer, System.fetch_env!("PD_WRITE_KEY")},
      json: %{event: event, distinct_id: distinct_id, properties: properties}
    )
  end)
end

Several apps, one project

Keep the applications of one product in one project. Ask your agent for a write key per application: one for iOS, one for Android, one for your website. Each key rotates and revokes on its own.

They share one event pool, which is the point. Send the same distinct_id from every application and a person who signs up on the web and acts in the app counts once in a funnel.

Every screen then offers an App picker, and every MCP read takes an optional app, so you can look at one application alone or at all of them together.

Prompt text
Create a product analytics source named Android app for the production environment. Show me the write key.

What to track

Track meaningful state changes, not interface noise. onboarding_completed, meal_logged, trial_started and subscription_started each answer a product question. button_clicked, modal_opened and screen_scrolled do not, and they crowd out the events that do.

Use the same name everywhere, in lower case with underscores. Put what varies in properties: platform, app_version, variant, source, plan, country.

Privacy

PostDeploy Analytics is designed for behavioural event data. Do not send sensitive personal information in event names, identifiers, or properties.

That means no names, email addresses, phone numbers, precise addresses, message content, health information, photos, payment details or authentication tokens.

distinct_id is whatever your application sends. Use an opaque identifier you generate. PostDeploy never derives an identity from an IP address, sets no cookie, fingerprints no device, and never joins one project's events to another's. The IP address of an ingest request is used to serve the request and is not stored on the event.

Limits

Event name
1 to 120 characters: letters, digits, _, ., :, -.
Distinct ID
Up to 200 characters. Optional, but unique-user counts need it.
Properties
Up to 40 per event. Names up to 64 characters, text values up to 512, 8 KB in total.
Property values
Text, numbers, or true/false. Nested objects and lists are refused.
Batch
Up to 100 events, 256 KB per request.
Timestamp
RFC 3339, within the last 30 days and not in the future. Defaults to arrival time.

Retries and duplicates

Retry on 429, on any 5xx, and on a network failure, with exponential backoff. Honour Retry-After when it is present.

Do not retry 400, 401 or 403: the request will not succeed until you change it.

Send your own id on each event to make a retry safe. PostDeploy writes an event with an id it has already stored exactly once, so a retried batch does not inflate your counts. A repeated event still counts once against your monthly allowance.

One event json
{
  "id": "evt_0192f3c4d5e6",
  "event": "subscription_started",
  "distinct_id": "anon_123"
}

Error responses

invalid_event
400. The event name is missing or malformed.
invalid_payload
400. The body is not the JSON shape this endpoint accepts.
invalid_properties
400. A property name, value, or count is outside the limits.
invalid_timestamp
400. The timestamp is malformed, too old, or in the future.
batch_too_large
400. More than 100 events in one batch.
invalid_api_key
401. The key is missing, unknown, revoked, or not a product analytics key.
plan_inactive
402. The organization has no active PostDeploy access.
body_too_large
413. The request body is over 256 KB.
rate_limit_exceeded
429. Back off and honour Retry-After.

Past your monthly allowance PostDeploy keeps answering 202 and reports what it dropped: {"accepted": 0, "dropped": 1, "dropped_reason": "quota"}. Your application never breaks because of an analytics limit.

If PostDeploy is unavailable

Analytics is best-effort client telemetry. Your application must keep working when PostDeploy does not answer, so never block a person's interaction on a capture call, and never raise from one.

Send your first signal today.

14 days free, no card required. Then $29 a month.

Start your 14-day free trial