Create Event
The Create Event endpoint allows you to log an audit event—a record of something that happened in your application. Each event captures who did what, to which entity, and when. Once created, events are immediately encrypted and stored securely, creating an immutable audit trail.
POST /api/ingest/event
When to Create Events
Create events whenever something important happens in your application that you want to track:
- User actions: Logins, profile updates, password changes
- Data changes: When invoices are created, updated, or deleted
- System events: Automated processes, scheduled jobs, webhook deliveries
- Security events: Failed login attempts, permission changes, API key rotations
- Business events: Payments processed, subscriptions activated, orders placed
Why log these? Audit logs help you:
- Meet compliance requirements (SOC 2, HIPAA, GDPR)
- Debug issues by seeing what happened before a problem occurred
- Provide transparency to users (activity feeds, transaction history)
- Investigate security incidents
- Understand user behavior and system usage
Request
Headers
| Header | Type | Required | Description |
|---|---|---|---|
Authorization | string | Yes* | Your project API key as Bearer token: Bearer pk_test_.... Recommended method. |
X-Project-Key | string | Yes* | Alternative: Your project API key as custom header. |
Content-Type | string | Yes | Must be application/json. |
Idempotency-Key | string | No | Optional idempotency key for safe retries. See Idempotency for details. |
X-Request-Id | string | No | Optional request ID for tracking and logging. |
*Either Authorization: Bearer <key> or X-Project-Key header must be provided.
Understanding Event Fields
Each event tells a story: who did what, to what, and when. Here's what each field means and why it matters:
actionKey (string, max 64 characters) - What happened?
A short identifier describing the action. Use a consistent format like entity.action (e.g., invoice.update, user.create, payment.processed).
Why this matters: The action key is how you'll filter and search events later. Consistent naming makes it easy to find all invoice updates or all user creations. Think of it as a category for the event.
Examples:
invoice.update- An invoice was modifieduser.login- A user logged inpayment.refund- A payment was refundedsubscription.cancel- A subscription was cancelled
Either actionKey or action (legacy) must be provided. Use actionKey for new integrations.
actionDescription (string, max 255 characters) - Human-readable details
A plain-English description of what happened. This is what you'll show to users in activity feeds or audit reports.
Why include this? While actionKey is great for filtering, actionDescription is what humans read. "Invoice status updated from draft to sent" is much clearer than just "invoice.update".
Example: "Invoice #12345 status changed from 'draft' to 'sent' by John Doe"
entityType (string, max 64 characters, required) - What was affected?
The type of thing that was acted upon. This groups related events together.
Why this matters: When you want to see "everything that happened to invoice inv_12345", you filter by entityType: "invoice" and entityId: "inv_12345". This makes it easy to build activity timelines for specific entities.
Examples: invoice, user, order, payment, subscription
entityId (string, max 128 characters, required) - Which specific item?
The unique identifier of the specific entity that was affected. This is how you identify the exact invoice, user, or order that was acted upon.
Why this matters: Combined with entityType, this lets you answer questions like "What happened to invoice inv_12345?" or "Show me all events for user usr_789".
actorType (string, enum: ["user", "service", "system"], default: "user", required) - Who or what performed the action?
The type of actor that performed the action:
user: A human user performed the actionservice: Another service or API performed the actionsystem: An automated system process performed the action
Why this matters: This helps you distinguish between actions taken by users vs. automated processes. For example, you might want to show only user actions in a customer-facing activity feed, but include all actions in an admin audit log.
actorId (string, max 128 characters, required) - Which specific actor?
The unique identifier of the specific user, service, or system that performed the action.
Why this matters: This lets you answer "What did user usr_123 do?" by filtering on actorId: "usr_123". It's essential for accountability and debugging.
actorDisplay (string, max 128 characters) - Human-readable actor name
The display name of the actor (e.g., "John Doe", "Payment Service", "Scheduled Job"). This is what you'll show to users.
Why include this? While actorId is great for filtering, actorDisplay is what humans see. "John Doe" is more meaningful than "usr_123" in a user-facing activity feed.
tenantId (string, max 64 characters) - Multi-tenant support
If your application serves multiple tenants (organizations, workspaces, etc.), use this to identify which tenant the event belongs to.
Why this matters: In multi-tenant applications, you need to ensure users can only see events for their own tenant. Filtering by tenantId ensures data isolation.
Example: If you're building a SaaS application, each customer organization would have its own tenantId.
occurredAt (string, format: date-time) - When did it happen?
The timestamp when the event actually occurred (ISO 8601 format). If not provided, defaults to the current time.
Why you might set this: If you're importing historical events or replaying events from a queue, you'll want to preserve the original timestamp. Otherwise, let it default to now.
source (string, max 64 characters) - Where did the event come from?
An identifier for the system or service that created the event (e.g., "web-app", "api", "background-job", "webhook-handler").
Why this matters: If you have multiple services creating events, the source helps you understand where events originated. This is useful for debugging and understanding your system architecture.
context (object, max 10KB) - Additional details
Any additional information that might be useful later. This is encrypted and stored separately from the main event.
Why use this? Sometimes you need to store extra details that don't fit in the standard fields:
- Request ID for tracing
- IP address for security investigations
- User agent for debugging
- Custom metadata specific to your application
Example:
{
"requestId": "req_abc123",
"ipAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0...",
"customField": "value"
}
changes (array, max 100 items) - What changed?
An array of field-level changes, showing what was modified. Each change includes the field path, the old value, and the new value.
Why this matters: Instead of just knowing "invoice was updated", you can see exactly what changed: "status changed from 'draft' to 'sent'" and "sentAt changed from null to '2024-01-15T10:30:00Z'". This level of detail is invaluable for debugging and compliance.
Example:
[
{
"op": "set",
"path": "status",
"before": "draft",
"after": "sent"
},
{
"op": "set",
"path": "sentAt",
"before": null,
"after": "2024-01-15T10:30:00Z"
}
]
changes[].op (string, enum: ["set", "unset", "add", "remove"], required)
Operation type.
changes[].path (string, max 256 characters, required)
JSON path to the changed field.
changes[].before (any, required)
Value before the change (max 5KB when serialized). Can be any JSON value.
changes[].after (any, required)
Value after the change (max 5KB when serialized). Can be any JSON value.
changes[].redacted (boolean)
Whether this change was redacted (for sensitive data).
Example Request
Using Authorization Bearer (Recommended):
curl -X POST https://api.archiva.app/api/ingest/event \
-H "Authorization: Bearer pk_test_your_api_key_here" \
-H "Idempotency-Key: idem_550e8400-e29b-41d4-a716-446655440000" \
-H "Content-Type: application/json" \
-d '{
"actionKey": "invoice.update",
"actionDescription": "Invoice status updated from draft to sent",
"tenantId": "tenant_abc123",
"entityType": "invoice",
"entityId": "inv_12345",
"actorType": "user",
"actorId": "usr_123",
"actorDisplay": "John Doe",
"occurredAt": "2024-01-15T10:30:00Z",
"source": "web-app",
"context": {
"requestId": "req_abc123",
"ipAddress": "192.168.1.1"
},
"changes": [
{
"op": "set",
"path": "status",
"before": "draft",
"after": "sent"
}
]
}'
Using X-Project-Key Header (Alternative):
curl -X POST https://api.archiva.app/api/ingest/event \
-H "X-Project-Key: pk_test_your_api_key_here" \
-H "Idempotency-Key: idem_550e8400-e29b-41d4-a716-446655440000" \
-H "Content-Type: application/json" \
-d '{
"actionKey": "invoice.update",
"actionDescription": "Invoice status updated from draft to sent",
"tenantId": "tenant_abc123",
"entityType": "invoice",
"entityId": "inv_12345",
"actorType": "user",
"actorId": "usr_123",
"actorDisplay": "John Doe",
"occurredAt": "2024-01-15T10:30:00Z",
"source": "web-app",
"context": {
"requestId": "req_abc123",
"ipAddress": "192.168.1.1"
},
"changes": [
{
"op": "set",
"path": "status",
"before": "draft",
"after": "sent"
}
]
}'
Response
Success (201 Created)
Event successfully created.
eventId (string, format: uuid)
Unique identifier of the ingested event.
receivedAt (string, format: date-time)
When the event was received by the server.
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"receivedAt": "2024-01-15T10:30:00.000Z"
}
Idempotent Replay (200 OK)
Event was already ingested with the same idempotency key and body. Returns the original event ID.
eventId (string, format: uuid)
Original event identifier (same as first request).
receivedAt (string, format: date-time)
Original timestamp when the event was first received.
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"receivedAt": "2024-01-15T10:30:00.000Z"
}
Error Responses
400 Bad Request
Invalid request (e.g., HTTPS required in production).
{
"error": "HTTPS required"
}
401 Unauthorized
Missing or invalid API key.
{
"error": "Missing API key. Provide Authorization: Bearer token or X-Project-Key header"
}
{
"error": "Invalid API key format"
}
{
"error": "Invalid API key"
}
403 Forbidden
API key revoked or monthly limit exceeded.
Revoked Key:
{
"error": "API key has been revoked"
}
Monthly Limit:
{
"error": "Monthly limit reached. Please upgrade your plan to continue sending events.",
"current": 1000,
"limit": 1000,
"plan": "free"
}
409 Conflict
Idempotency key conflict (same key used with different body).
{
"error": "Idempotency key conflict: same key used with different body"
}
413 Payload Too Large
Request payload exceeds 1MB limit.
{
"error": "Request payload too large"
}
422 Validation Failed
Request validation failed.
error (string)
Error message.
details (array)
Array of validation errors.
details[].path (array)
JSON path to the field that failed validation.
details[].message (string)
Human-readable error message.
details[].code (string)
Error code (e.g., 'invalid_type', 'custom').
{
"error": "Validation failed",
"details": [
{
"path": ["entityId"],
"message": "Required",
"code": "invalid_type"
},
{
"path": [],
"message": "Either actionKey or action must be provided",
"code": "custom"
}
]
}
429 Too Many Requests
Rate limit exceeded. See Rate Limiting for details.
Response Headers:
Retry-After: 45
{
"error": "Rate limit exceeded"
}
500 Internal Server Error
Internal server error.
{
"error": "Internal server error"
}
Constraints
- Request size: Maximum 1MB
- Context size: Maximum 10KB when serialized
- Change size: Each
before/aftervalue maximum 5KB when serialized - Changes array: Maximum 100 changes per event
- Rate limit: 100 requests per 60 seconds per API key
- Monthly limits: Based on billing plan (free: 1,000, start: 25,000, pro: 150,000)
Notes
- Events are encrypted at rest using project-specific encryption keys
- All string fields are encoded for additional security
- Idempotency keys expire after 24 hours
- The
actionfield is maintained for backward compatibility butactionKeyis preferred - In production, HTTPS is required