UPPU API documentation

Webhooks

UPPU sends every event to your HTTPS endpoint as a signed POST request, one event per request. This page explains how to set up an endpoint, verify the signature, answer and retry, catch up after an outage, and what every event looks like.

Set up an endpoint

The company owner manages endpoints in the UPPU panel: the company's settings, then Webhooks (after confirming the password), or with an API key through the API. A company has at most 5 endpoints.

Activation. A new endpoint receives events only after two checks, both within the last 60 minutes:

  1. Deploy your receiver with signature verification.
  2. Send test event: UPPU sends a sample event signed with your secret; your endpoint must answer 2xx.
  3. Send wrong-signature probe: UPPU sends webhook.test signed with a wrong secret; your endpoint must refuse it with any status other than 2xx. This proves that you check the signature.
  4. Activate.

Events created while an endpoint is not active are not delivered to it; fetch them with GET /v1/events.

UPPU's outgoing address is shown on the Webhooks page of the panel and announced 30 days before it changes. Do not rely on an IP filter alone: the signature is what proves that an event comes from UPPU.

Manage endpoints through the API

An API key with the webhooks:manage scope manages the same endpoints as the panel at /v1/webhook-endpoints: list, create, change and delete them, send a test event or the wrong-signature probe, activate and disable them, and rotate the signing secret. No preset includes the scope and existing keys do not have it: the owner adds it to a key with Custom on the Developers page. The rules, the limit of 5 endpoints and the audit records are those of the panel; one endpoint more gets 409 plan_limit_reached.

Weekly test event

An endpoint can opt in to a weekly test event: the endpoint form of the panel, or weekly_test: true through the API (off by default). Once a week UPPU then sends webhook.test with test: true, signed with the endpoint's secret, to the endpoint while it is active or failing. A failed weekly test is only a warning: the panel shows it on the endpoint and the API in weekly_test.failed_at. The endpoint is never suspended for it and deliveries go on. A passing weekly test or test event, turning the weekly test off and a new URL clear the warning.

The request

Header Value
Content-Type application/json; charset=utf-8
User-Agent UPPU-Webhooks/1.0
webhook-id The event id, the same across retries, resends and endpoints; equal to the body's id
webhook-timestamp Unix seconds, new on every attempt
webhook-signature v1,<base64>; during a secret rotation two values separated by a space
Uppu-Event-Type The event type, for example message.status
Uppu-Delivery-Id This delivery to this endpoint; a resend keeps it
Uppu-Attempt 1 for the first attempt

The body is compact UTF-8 JSON, the same bytes on every attempt:

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6a",
  "object": "event",
  "type": "message.received",
  "api_version": "v1",
  "created_at": "2026-10-20T09:15:01.020Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": {"id": "01j9z0ch5k2m4n6p8q0r2s4t6v", "type": "whatsapp"},
  "test": false,
  "data": {}
}

Verify the signature

UPPU signs every request the Standard Webhooks way:

  1. Key: remove whsec_ from the secret and base64-decode the rest. The key is the decoded bytes, not the text of the secret.
  2. Signed content: webhook-id + . + webhook-timestamp + . + the raw body, exactly as received. Never decode and encode the JSON again before checking.
  3. Signature: HMAC-SHA256 of the signed content with the key, standard base64 with padding.
  4. Compare: split webhook-signature at spaces; a part v1,<value> whose value equals your signature, compared in constant time, makes the request valid.
  5. Refuse a request without webhook-id, with a timestamp that is not digits only, or with a timestamp more than 300 seconds away from your clock (keep the clock in sync with NTP).

During a secret rotation UPPU signs with both the new and the old secret for 24 hours, so either secret verifies the request. Switch your receiver to the new secret within those 24 hours.

Ready code: PHP, Node.js, Python and C#.

Test vector. The published test vector of the Standard Webhooks specification checks an implementation offline:

Value
Secret whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw
webhook-id msg_p5jXN8AQM9LWM0D4loKWxJek
webhook-timestamp 1614265330
Body {"test": 2432232314}
webhook-signature v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=

Check it with your clock set to the vector's timestamp; the secret is a public example, never a real one.

Answer quickly

Retries, ordering and suspension

Missed events

GET /v1/events (scope events:read, 2 requests per second per key) returns the events of the last 7 days with the same ids and bodies as the webhooks, oldest first.

Events

EventSubscribed by defaultOrdered per
message.receivedyescontact
message.statusyescontact
message.creatednocontact
contact.identity_changedyescontact
contact.consent_changednocontact
contact.erasedyescontact
template.updatednotemplate
channel.status_changedyeschannel
channel.health_changednochannel
conversation.updatednocontact
contact.reachability_changedyescontact
contact.updated (not produced yet)nocontact
account.service_stop_scheduledyescompany account
account.service_resumedyescompany account
webhook.testalways sentnot ordered

Unknown event types and fields can appear within v1: ignore them. The examples below show whole events; ids and values are examples.

message.received

A person sent a message on any channel. data.message is the message, data.contact the person (identities holds the current identity per type on this channel, for example phone, bsuid, psid, igsid, telegram or web_visitor), data.conversation the conversation and data.service_window the reply window (null on web chat and Telegram).

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6a",
  "object": "event",
  "type": "message.received",
  "api_version": "v1",
  "created_at": "2026-10-20T09:15:01.020Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": {
    "id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
    "type": "whatsapp"
  },
  "test": false,
  "data": {
    "message": {
      "object": "message",
      "id": "01jab3t6ms2m4n6p8q0r2s4t6v",
      "channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
      "channel_type": "whatsapp",
      "conversation_id": "01jab39zcv2m4n6p8q0r2s4t6v",
      "contact_id": "01j8x7ct5k2m4n6p8q0r2s4t6v",
      "direction": "in",
      "author": {
        "type": "contact",
        "label": "Deniz",
        "api_key_prefix": null
      },
      "content": {
        "type": "text",
        "text": {
          "body": "Hello, where is my order?"
        }
      },
      "context": null,
      "template": null,
      "preview": "Hello, where is my order?",
      "status": "received",
      "status_changed_at": null,
      "error": null,
      "client_reference": null,
      "provider_message_id": "PROVIDER-MESSAGE-ID",
      "sent_at": "2026-10-20T09:15:00.000Z",
      "received_at": "2026-10-20T09:15:01.000Z",
      "created_at": "2026-10-20T09:15:01.000Z"
    },
    "contact": {
      "id": "01j8x7ct5k2m4n6p8q0r2s4t6v",
      "display_name": "Deniz",
      "identities": {
        "phone": "+447700900100",
        "bsuid": "TR.ExampleUser1"
      }
    },
    "conversation": {
      "id": "01jab39zcv2m4n6p8q0r2s4t6v",
      "status": "open",
      "handling": "api"
    },
    "service_window": {
      "open": true,
      "closes_at": "2026-10-21T09:13:00.000Z"
    }
  }
}

message.status

A status of an outbound message moved forward: accepted, unknown, sent, delivered, read, failed or expired (never queued). It is sent for every author: your API key, the team, UPPU Bot, a campaign of the panel. Match it to your send by data.message.id or data.message.client_reference; on failed, data.error.code says why. expired (code ttl_expired) follows a send with a ttl that UPPU could not hand to the provider in time: nothing was sent, and previous_status is queued. If an earlier attempt got an error answer from the provider, the ttl ends the send as failed with that error instead. In the rare case that the provider took the message after all (UPPU could not reach it and counted the attempt as not delivered), a later status of that attempt follows the expired. A campaign message (author type campaign) that had not reached the provider when the campaign was paused or cancelled also ends as expired (code ttl_expired), or as failed with the error of an earlier attempt.

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6b",
  "object": "event",
  "type": "message.status",
  "api_version": "v1",
  "created_at": "2026-10-20T09:20:03.010Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": {
    "id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
    "type": "whatsapp"
  },
  "test": false,
  "data": {
    "message": {
      "id": "01jab3a1k9f2m4n6p8q0r2s4t6",
      "channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
      "conversation_id": "01jab39zcv2m4n6p8q0r2s4t6v",
      "contact_id": "01j8x7ct5k2m4n6p8q0r2s4t6v",
      "provider_message_id": "PROVIDER-MESSAGE-ID",
      "client_reference": "order-1001",
      "author": {
        "type": "api",
        "api_key_prefix": "Ab12Cd34Ef56"
      }
    },
    "status": "delivered",
    "previous_status": "sent",
    "occurred_at": "2026-10-20T09:20:03.004Z",
    "error": null
  }
}

message.created

An outbound message was queued: by an agent in the inbox, by UPPU, by UPPU Bot, by a campaign of the panel (author type campaign, bulk template messages) or by an API key. data has the shape of message.received, with direction out and status queued. Use it to mirror the team's replies in your own system.

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6c",
  "object": "event",
  "type": "message.created",
  "api_version": "v1",
  "created_at": "2026-10-20T09:20:00.130Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": {
    "id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
    "type": "whatsapp"
  },
  "test": false,
  "data": {
    "message": {
      "object": "message",
      "id": "01jab3a1k9f2m4n6p8q0r2s4t6",
      "channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
      "channel_type": "whatsapp",
      "conversation_id": "01jab39zcv2m4n6p8q0r2s4t6v",
      "contact_id": "01j8x7ct5k2m4n6p8q0r2s4t6v",
      "direction": "out",
      "author": {
        "type": "api",
        "label": "Order bot",
        "api_key_prefix": "Ab12Cd34Ef56"
      },
      "content": {
        "type": "text",
        "text": {
          "body": "Your order has shipped.",
          "preview_url": false
        }
      },
      "context": null,
      "template": null,
      "preview": "Your order has shipped.",
      "status": "queued",
      "status_changed_at": "2026-10-20T09:20:00.120Z",
      "error": null,
      "client_reference": "order-1001",
      "provider_message_id": null,
      "sent_at": null,
      "received_at": null,
      "created_at": "2026-10-20T09:20:00.120Z"
    },
    "contact": {
      "id": "01j8x7ct5k2m4n6p8q0r2s4t6v",
      "display_name": "Deniz",
      "identities": {
        "phone": "+447700900100",
        "bsuid": "TR.ExampleUser1"
      }
    },
    "conversation": {
      "id": "01jab39zcv2m4n6p8q0r2s4t6v",
      "status": "open",
      "handling": "api"
    },
    "service_window": {
      "open": true,
      "closes_at": "2026-10-21T09:13:00.000Z"
    }
  }
}

contact.identity_changed

A person's WhatsApp user id changed. data.contact_id stays the same; update a stored identity from previous to current. reason names what reported the change: usually user_changed_user_id (WhatsApp's notice in the conversation), user_changed_number when the person changed their phone number (WhatsApp then gives them a new user id too), and user_id_update from the staging simulator. Accept reasons you do not know.

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6d",
  "object": "event",
  "type": "contact.identity_changed",
  "api_version": "v1",
  "created_at": "2026-10-20T10:00:00.050Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": {
    "id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
    "type": "whatsapp"
  },
  "test": false,
  "data": {
    "contact_id": "01j8x7ct5k2m4n6p8q0r2s4t6v",
    "channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
    "previous": {
      "type": "bsuid",
      "value": "TR.ExampleUser1"
    },
    "current": {
      "type": "bsuid",
      "value": "TR.ExampleUser2"
    },
    "reason": "user_id_update",
    "occurred_at": "2026-10-20T10:00:00.000Z"
  }
}

contact.consent_changed

A consent record was added: through the API, by a person who wrote STOP, DUR or İPTAL, by WhatsApp's marketing opt-out, by a person who stopped the business's marketing messages in WhatsApp (source is meta_user_preferences; marketing only, and turning them back on in WhatsApp is not a consent: no record, no event), by a team member in the UPPU panel (source is agent_recorded) or from a CSV import (source is import; one event per new record). The event reports the record that was written, not the person's current state: a consent collected before a newer withdrawal is still recorded and sends a granted event, and the withdrawal stays in force. Read the current state from the consents summary of GET /v1/contacts/{id}. UPPU applies the consent rules itself; use the event to keep your own lists in step.

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6e",
  "object": "event",
  "type": "contact.consent_changed",
  "api_version": "v1",
  "created_at": "2026-10-20T09:16:00.030Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": {
    "id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
    "type": "whatsapp"
  },
  "test": false,
  "data": {
    "contact_id": "01j8x7ct5k2m4n6p8q0r2s4t6v",
    "consent": {
      "id": "01jab3cn5t2m4n6p8q0r2s4t6v",
      "channel_type": "whatsapp",
      "purpose": "marketing",
      "state": "withdrawn",
      "source": "inbound_keyword",
      "recorded_at": "2026-10-20T09:16:00.000Z"
    }
  }
}

contact.erased

UPPU erased a person (for example on a data protection request). Delete or anonymize your copy of the person.

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6f",
  "object": "event",
  "type": "contact.erased",
  "api_version": "v1",
  "created_at": "2026-10-20T11:00:00.000Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": {
    "id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
    "type": "whatsapp"
  },
  "test": false,
  "data": {
    "contact_id": "01j8x7ct5k2m4n6p8q0r2s4t6v"
  }
}

template.updated

The status, category or quality of a WhatsApp template changed. Stop sending a template that is PAUSED or REJECTED. The envelope's channel is null.

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6g",
  "object": "event",
  "type": "template.updated",
  "api_version": "v1",
  "created_at": "2026-10-20T08:00:00.000Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": null,
  "test": false,
  "data": {
    "template": {
      "object": "template",
      "id": "01jab3tp1a2m4n6p8q0r2s4t6v",
      "name": "order_update",
      "language": "tr",
      "category": "UTILITY",
      "status": "PAUSED",
      "quality": "YELLOW",
      "channel_ids": [
        "01j9z0ch5k2m4n6p8q0r2s4t6v"
      ]
    },
    "change": {
      "field": "status",
      "previous": "APPROVED",
      "current": "PAUSED",
      "reason": null
    }
  }
}

channel.status_changed

A channel's status or mode changed, for example it was connected, paused or disconnected. reason explains why (for example token_invalid, disconnected, connected); accept reasons you do not know.

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6h",
  "object": "event",
  "type": "channel.status_changed",
  "api_version": "v1",
  "created_at": "2026-10-20T09:00:00.000Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": {
    "id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
    "type": "whatsapp"
  },
  "test": false,
  "data": {
    "channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
    "status": "paused",
    "previous_status": "active",
    "mode": "live",
    "previous_mode": "live",
    "reason": "token_invalid"
  }
}

channel.health_changed

The health summary of a channel changed (green, amber, red, unknown); reasons lists the checks that are not green.

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6j",
  "object": "event",
  "type": "channel.health_changed",
  "api_version": "v1",
  "created_at": "2026-10-20T09:00:00.500Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": {
    "id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
    "type": "whatsapp"
  },
  "test": false,
  "data": {
    "channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
    "health": {
      "status": "red",
      "previous_status": "green",
      "reasons": [
        "token_invalid"
      ],
      "checked_at": "2026-10-20T09:00:00.000Z"
    },
    "messaging_limit": "TIER_1K",
    "quality": "GREEN"
  }
}

conversation.updated

A conversation changed: who holds it, its status or its assignment. data.conversation is the conversation as GET /v1/conversations/{id} returns it at that moment, data.change what changed, data.reason why and data.actor who did it (type member, api with your key's api_key_prefix, bot or system). New endpoints leave this event off: subscribe to it explicitly. UPPU writes it only while one of your endpoints subscribes to it, so GET /v1/events has it only then too.

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6k",
  "object": "event",
  "type": "conversation.updated",
  "api_version": "v1",
  "created_at": "2026-10-20T12:00:00.012Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": {
    "id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
    "type": "whatsapp"
  },
  "test": false,
  "data": {
    "conversation": {
      "object": "conversation",
      "id": "01jab39zcv2m4n6p8q0r2s4t6v",
      "channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6v",
      "contact_id": "01j8x7ct5k2m4n6p8q0r2s4t6v",
      "status": "open",
      "handling": "api",
      "assigned": false,
      "handed_over_at": "2026-10-20T12:00:00.000Z",
      "held_by": "team",
      "handover_reason": "api",
      "opened_at": "2026-10-20T11:55:00.000Z",
      "resolved_at": null,
      "last_inbound_at": "2026-10-20T11:55:00.000Z",
      "last_message_at": "2026-10-20T11:56:00.000Z",
      "service_window": {
        "open": true,
        "closes_at": "2026-10-21T11:53:00.000Z"
      }
    },
    "change": "handed_over",
    "reason": "api",
    "actor": {
      "type": "api",
      "api_key_prefix": "k7Hq2mP0aZ1x"
    },
    "occurred_at": "2026-10-20T12:00:00.000Z"
  }
}

contact.reachability_changed

Telegram: a person blocked or unblocked the bot, or Telegram refused a send. reachable is false with blocked_by_contact or send_forbidden, and true with unblocked_by_contact or contact_wrote.

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6m",
  "object": "event",
  "type": "contact.reachability_changed",
  "api_version": "v1",
  "created_at": "2026-10-21T10:00:00.040Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": {
    "id": "01j9z0tg5k2m4n6p8q0r2s4t6v",
    "type": "telegram"
  },
  "test": false,
  "data": {
    "contact_id": "01j8x7ct5k2m4n6p8q0r2s4t6v",
    "channel_id": "01j9z0tg5k2m4n6p8q0r2s4t6v",
    "identity": {
      "type": "telegram",
      "value": "123456789"
    },
    "reachable": false,
    "reason": "blocked_by_contact",
    "occurred_at": "2026-10-21T10:00:00.000Z"
  }
}

contact.updated

A person's contact changed: the name, the identities (an identity added or replaced, for example a WhatsApp user id or the user id your website signed), the company's contact fields, or a merge and its undo. data.contact is the contact as GET /v1/contacts/{id} returns it at that moment to a key for all channels, data.changes the fields that changed (display_name, identities, merged_into, notes, attributes; never the earlier values) and data.reason what changed it (member_edited, provider_name, pre_chat, identity_added, identity_rotated, signed_handoff, merged or merge_reverted). New endpoints leave this event off: subscribe to it explicitly. UPPU writes it only while one of your endpoints subscribes to it, so GET /v1/events has it only then too.

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6m",
  "object": "event",
  "type": "contact.updated",
  "api_version": "v1",
  "created_at": "2026-10-26T12:00:00.012Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": null,
  "test": false,
  "data": {
    "contact": {
      "object": "contact",
      "id": "01j8x7ct5k2m4n6p8q0r2s4t6v",
      "display_name": "Ayşe Yılmaz",
      "blocked": false,
      "merged_into": null,
      "identities": [
        {
          "channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6w",
          "type": "web_visitor",
          "value": "wv_01jab3vs5k2m4n6p8q0r2s4t6v",
          "current": true
        },
        {
          "channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6w",
          "type": "external",
          "value": "user-1001",
          "current": true
        }
      ],
      "service_windows": [],
      "consents": [],
      "created_at": "2026-10-25T09:30:00.000Z",
      "notes": null,
      "attributes": {
        "customer_no": "A-1001"
      }
    },
    "changes": [
      "display_name",
      "identities"
    ],
    "reason": "signed_handoff",
    "occurred_at": "2026-10-26T12:00:00.000Z"
  }
}

account.service_stop_scheduled

The company's service stops for billing at data.stop_at unless UPPU records the payment first: its payment is long overdue (reason billing_overdue) or its subscription has ended (subscription_ended). It is sent with UPPU's stop warning, at least 72 hours before the stop, which always falls at 10:00 Turkey time on a working day. From then on every API response to a caller that passes the key's IP allow-list carries the header UPPU-Service-Stop-At with the stop instant in force. The instant only ever moves later: if UPPU's warning reached nobody and goes again, a second account.service_stop_scheduled names the later instant, and the header always carries the current one.

{
  "id": "01jace0s7p2m4n6p8q0r2s4t6m",
  "object": "event",
  "type": "account.service_stop_scheduled",
  "api_version": "v1",
  "created_at": "2026-10-26T07:00:00.012Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": null,
  "test": false,
  "data": {
    "stop_at": "2026-10-29T07:00:00.000Z",
    "reason": "billing_overdue",
    "occurred_at": "2026-10-26T07:00:00.000Z"
  }
}

account.service_resumed

A service stopped for billing runs again: stopped_at is when it stopped, resumed_at when it opened again. Resend the deliveries that died with service_stopped from the panel; nothing is sent again by itself. Like account.service_stop_scheduled, it is on by default for new endpoints and goes to every subscribed endpoint, whatever its channel filter.

{
  "id": "01jace0r3s2m4n6p8q0r2s4t6m",
  "object": "event",
  "type": "account.service_resumed",
  "api_version": "v1",
  "created_at": "2026-10-30T11:42:10.512Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": null,
  "test": false,
  "data": {
    "stopped_at": "2026-10-29T07:00:00.000Z",
    "resumed_at": "2026-10-30T11:42:10.500Z",
    "occurred_at": "2026-10-30T11:42:10.500Z"
  }
}

webhook.test

Sent by the test button and the wrong-signature probe of the panel and of the API, and by the weekly test event, always with test: true. It is not stored and never appears in GET /v1/events.

{
  "id": "01jace0e1v2m4n6p8q0r2s4t6n",
  "object": "event",
  "type": "webhook.test",
  "api_version": "v1",
  "created_at": "2026-10-20T09:15:02.481Z",
  "team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
  "channel": null,
  "test": true,
  "data": {
    "note": "UPPU test olayı",
    "endpoint_id": "01jab3ep5k2m4n6p8q0r2s4t6v"
  }
}