Backend Events
Send durable server-side product events to SafetyKit.
Beta.
SafetyKit uses backend events to understand meaningful product actions such as account creation, account updates, user-to-user contact, content uploads, product listing changes, user reports, and moderation decisions. Send events from your backend after the action has happened in your system. Do not send backend events directly from a browser or mobile client.
Use the Events API reference for the full request schema, SDK examples, and response fields.
Quick Start
Section titled “Quick Start”Send one event, or an ordered batch of events, to:
POST https://api.safetykit.com/v1/eventsAuthorization: Bearer <SAFETYKIT_API_KEY>Content-Type: application/jsonFor a faster response, send the same request body to POST https://api.safetykit.com/v1/async/events. SafetyKit returns after the events are queued, before they have been fully ingested.
Single event:
{ "type": "user_contact", "event_name": "message_sent", "user_id": "user_123", "target_user_id": "user_456", "timestamp": "2026-05-21T00:15:15.000Z", "content_id": "message_abc123", "content": [ { "type": "text", "key": "body", "text": "Hey, is this still available?" } ], "metadata": { "conversation_id": "conversation_789", "channel": "marketplace_dm" }, "client_info": { "ip": "203.0.113.7" }}Response:
{ "status": "ok", "event_id": "01KSR4C8JN1SA9X63T6P368K6W"}Batch event request:
[ { "type": "update_account", "event_name": "profile_updated", "user_id": "user_123", "timestamp": "2026-05-21T00:15:10.000Z", "metadata": { "changed_fields": ["bio"] } }, { "type": "user_contact", "event_name": "message_sent", "user_id": "user_123", "target_user_id": "user_456", "timestamp": "2026-05-21T00:15:15.000Z", "content_id": "message_abc123" }]Response:
{ "status": "ok", "event_ids": ["01KSR4C8JN1SA9X63T6P368K6W", "01KSR4CB1MFVX1TAYM46YQXZ3E"]}SafetyKit ingests every event in a batch before returning a response.
Event Shape
Section titled “Event Shape”Every event is a JSON object:
{ "type": "user_contact", "event_name": "message_sent", "user_id": "user_123", "target_user_id": "user_456", "timestamp": "2026-05-21T00:15:15.000Z", "content_id": "message_abc123", "content": [], "resources_used": [], "metadata": {}}Required base fields:
type: one ofuser_contact,content_uploaded,product_changed,create_account,update_account,user_report, ormoderation_decisionevent_name: a stable, low-cardinality snake_case name for the product actiontimestamp: the time the event occurred in your system, as an ISO 8601 datetime stringuser_id: your canonical stable identifier for the user or account performing the action. Foruser_report, this is the reporter. Do not senduser_idformoderation_decision.
Use stable event names like message_sent or profile_updated. Put dynamic values in typed fields, content, resources_used, or metadata.
Common optional fields:
target_user_id: the other user in a user-to-user interaction, the user being reported foruser_report, or the user being moderated formoderation_decisiontarget_content_id: foruser_reportormoderation_decision, your stable identifier for the content being reported or moderatedcontent_id: your stable identifier for the content involved in the event, such as a message, listing, page, post, profile, uploaded media item, or report objectcontent: user-authored or user-uploaded content SafetyKit should compare or analyzeresources_used: reusable identifiers or real-world resources associated with the eventmetadata: freeform non-PII product context for filtering, segmentation, debugging, and investigationclient_info: client context observed by your backend for the user action that triggered the event, such as the client IP address
Include content_id whenever you send content and your system has a stable ID for that content. SafetyKit stores this alongside the computed content hash so you can later report, search, join, or reconcile the content in your system.
Event Types
Section titled “Event Types”user_contact
Section titled “user_contact”Use when one user contacts, transacts with, or otherwise interacts with another user.
Required fields:
user_idtarget_user_id
Recommended fields:
content, if the interaction includes a message or user-authored notecontent_id, if the interaction includes a message or other stable content objectmetadata, for conversation IDs, transaction IDs, channel names, or similar contextclient_info, for the IP address your backend observed for the interaction
Example:
{ "type": "user_contact", "event_name": "message_sent", "user_id": "sender_123", "target_user_id": "recipient_456", "timestamp": "2026-05-21T00:15:15.000Z", "content_id": "message_abc123", "content": [ { "type": "text", "key": "body", "text": "Hey, is this still available?" } ], "metadata": { "conversation_id": "conversation_789" }}create_account
Section titled “create_account”Use when a user creates an account.
Required fields:
user_id
Recommended fields:
resources_used, for signup emails, phones, names, addresses, URLs, or social handlescontent, for public bio or profile textcontent_id, if you include profile, bio, media, or other content with a stable ID in your systemmetadata, for signup source, cohort, or other event-local contextclient_info, for the IP address your backend observed for the signup
update_account
Section titled “update_account”Use when a user updates account or profile information.
Required fields:
user_id
Recommended fields:
resources_used, for changed emails, phones, social handles, addresses, URLs, or payment identifierscontent, for changed public bio or profile textcontent_id, if the changed profile, bio, media, or other content has a stable ID in your systemmetadata, for changed field names, visibility state, or other event-local contextclient_info, for the IP address your backend observed for the change
Example:
{ "type": "update_account", "event_name": "profile_updated", "user_id": "user_123", "timestamp": "2026-05-21T00:15:15.000Z", "content_id": "profile_user_123", "content": [ { "type": "text", "key": "bio", "text": "Independent designer making limited-run products." } ], "metadata": { "changed_fields": ["bio"], "profile_visibility": "public" }, "client_info": { "ip": "203.0.113.7" }}content_uploaded
Section titled “content_uploaded”Use when a user posts, uploads, publishes, edits, or replaces content.
Required fields:
user_idcontent
Recommended fields:
content_id, for the listing, post, page, profile, upload, media item, or other content object being added or changedmetadata, for listing IDs, post IDs, visibility state, category, or similar product contextclient_info, for the IP address your backend observed for the upload
Example:
{ "type": "content_uploaded", "event_name": "project_story_updated", "user_id": "creator_456", "timestamp": "2026-05-21T00:15:15.000Z", "content_id": "project_789_story", "content": [ { "type": "text", "key": "project_story", "text": "We are building a new limited edition product." }, { "type": "image", "key": "hero_image", "source": { "type": "url", "url": "https://cdn.example.com/project/hero.jpg" } } ], "metadata": { "project_id": "project_789", "visibility": "public" }}product_changed
Section titled “product_changed”Use when a product listing is created or updated. This is content_uploaded plus typed product commerce fields.
Required fields:
user_idcontentproduct, withprice(amountas a decimal string andcurrencyas an ISO 4217 code), optionaldiscounted_price, and optionalshipping_info(ships_fromstring andships_toarray of strings)
event_name must be one of product_created or product_updated.
When a product_changed event is accepted, SafetyKit sends an event.product_changed webhook containing the full original event.
Example:
{ "type": "product_changed", "event_name": "product_created", "user_id": "seller_123", "timestamp": "2026-05-21T00:15:15.000Z", "content_id": "product_abc123", "content": [ { "type": "text", "key": "description", "text": "Vintage leather jacket" }, { "type": "image", "key": "primary_photo", "source": { "type": "url", "url": "https://cdn.example.com/products/abc123.jpg" } } ], "product": { "price": { "amount": "500.00", "currency": "usd" }, "discounted_price": { "amount": "49.99", "currency": "usd" }, "shipping_info": { "ships_from": "us", "ships_to": ["us", "ca"] } }, "metadata": { "category": "apparel" }}user_report
Section titled “user_report”Use when a user reports another user or a piece of content.
Required fields:
user_idlabels
Recommended fields:
target_user_id, for the user being reportedtarget_content_id, for the content being reportedcontent_id, for your stable identifier for the report objectcontent, for report text, screenshots, evidence, or other explanatory content SafetyKit should compare or analyzemetadata, for report surface, source, queue, or other product context
A report should include target_user_id, target_content_id, or both. Prefer both when the report is about a content item and you know the user associated with that content.
Example:
{ "type": "user_report", "event_name": "content_report_submitted", "user_id": "reporter_789", "target_user_id": "seller_456", "target_content_id": "listing_abc123", "labels": ["spam"], "content_id": "report_abc123", "timestamp": "2026-05-21T00:15:15.000Z", "content": [ { "type": "text", "key": "report_reason", "text": "This listing looks like spam." } ], "metadata": { "report_surface": "listing_page" }}SafetyKit stores the canonical report event and records one label row per report label.
moderation_decision
Section titled “moderation_decision”Use when an automated enforcement system or manual review workflow labels or decides on a user or piece of content.
Required fields:
source_typesource_idlabels
Recommended fields:
target_user_id, for the user being moderatedtarget_content_id, for the content being moderatedcontent_id, for your stable identifier for the decision objectcontent, for review notes, decision rationale, evidence, or other explanatory content SafetyKit should compare or analyzemetadata, for outcome, confidence, queue, rule, model version, or other product context
A moderation decision should include target_user_id, target_content_id, or both. Do not include user_id; use source_type and source_id for attribution.
source_type is the source category for the decision. Supported values:
human_moderator: a human reviewer in your moderation workflowexpert_labeler: an expert human labeler or specialist reviewerautomation: an automated model, rules engine, or enforcement workflowvendor: an external review or moderation provider
source_id is the stable identifier within source_type. Examples: moderator@example.com or 550e8400-e29b-41d4-a716-446655440000 for expert_labeler or human_moderator, automated_review_v2026_06_04 for automation, or <vendor_name>-v2.1 for vendor.
Example:
{ "type": "moderation_decision", "event_name": "content_moderation_decided", "source_type": "automation", "source_id": "automated_review_v2026_06_04", "target_user_id": "seller_456", "target_content_id": "listing_abc123", "labels": ["spam"], "content_id": "decision_abc123", "timestamp": "2026-05-21T00:15:15.000Z", "metadata": { "decision": "remove", "confidence": "high" }}Content
Section titled “Content”Use content for user-authored or user-uploaded content SafetyKit should compare or analyze.
Text part:
{ "type": "text", "key": "body", "text": "Hey, is this still available?"}Image part:
{ "type": "image", "key": "profile_photo", "source": { "type": "url", "url": "https://cdn.example.com/images/profile.jpg" }}Guidelines:
- Split structured content into meaningful parts.
- Use stable
keyvalues such asbody,title,description,bio, orprimary_photo. - Include the top-level
content_idwhenever the content has a stable ID in your system. - Do not add IDs or timestamps to text just to make it unique.
- Send original user-authored text rather than rendered HTML when possible.
- Send canonical media URLs when available.
Resources
Section titled “Resources”Use resources_used for reusable identifiers associated with the event.
[ { "type": "email", "value": "user@example.com" }, { "type": "phone", "value": "+12069406843" }, { "type": "url", "value": "https://example.com/profile/user_123" }]Common resource types include email, phone, name, address, url, facebook, instagram, x, tiktok, youtube, linkedin, and payment_instrument.
Guidelines:
- Send values exactly as your backend receives or stores them.
- Prefer E.164 phone numbers if your system already has them.
- Prefer canonical URLs if your system already has them.
- Do not put multiple resources in one string.
- Do not duplicate the same value in both
resources_usedandmetadata.
Metadata
Section titled “Metadata”Use metadata for freeform non-PII product context for filtering, segmentation, debugging, and investigation.
{ "conversation_id": "conversation_789", "channel": "marketplace_dm", "is_first_message": true, "message_count_24h": 12}Metadata values may be strings, numbers, booleans, or arrays of strings, numbers, or booleans. Metadata keys should be stable snake_case strings.
Do not put raw PII, message bodies, bios, addresses, image URLs, or reusable identifiers in metadata. Use content or resources_used. Put the client IP address in client_info.
Client Info
Section titled “Client Info”Use client_info for client context observed by your backend for the user action that triggered the event. Every event type accepts it.
{ "ip": "203.0.113.7"}ip: the IP address observed by your backend for the client that triggered the event
SafetyKit links events sharing an IP address, so sending client_info.ip on signups, account changes, contacts, and uploads helps surface coordinated activity such as mass account creation or ban evasion.
Guidelines:
- Send the address your backend attributes to the end client, such as the connecting address after resolving trusted proxy headers.
- Both IPv4 and IPv6 addresses are accepted.
- Send
client_info.ipon every event where your backend observed the client, not just signups.
Operational Guidance
Section titled “Operational Guidance”- Send events after durable writes in your own system.
- For high-volume systems, write events to your own outbox or queue and have a worker send them to SafetyKit.
- Retry transient network errors and 5xx responses with exponential backoff.
- Do not retry 4xx responses without changing the request.
- Store your own source event identifier in
metadataif you need replay or reconciliation.