Project Event Ingestion Guide
This guide explains how an application sends events to Givemora using a project credential. It covers project setup, the event API, batch requests, signed requests, the standard event catalog, payload rules, responses, retries, and troubleshooting.
Base URL: https://givemora.com
1. Set up a project credential
Create or select a project in the Givemora owner workspace. Issue a project credential for each environment the application uses. Supported environments are development, staging, and production; the credential determines the event environment, so do not send an environment property in the event body.
You can issue a credential in the owner dashboard or with the management API. Management tokens and project credentials are different:
- A management token, usually prefixed
gm_owner_, manages workspace and project resources. It is not an event-ingestion key. - A project credential, prefixed
gm_project_, has theevents:writescope and sends events for one project and one environment.
To issue a project credential through the management API, use a management token with credentials:manage scope and the project ID:
export GIVEMORA_MANAGEMENT_TOKEN='gm_owner_...'
export GIVEMORA_PROJECT_ID='your-project-id'
curl --fail-with-body --request POST \
"https://givemora.com/v1/manage/projects/${GIVEMORA_PROJECT_ID}/credentials" \
--header "Authorization: Bearer ${GIVEMORA_MANAGEMENT_TOKEN}" \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: issue-production-ingest-key-001' \
--data '{"name":"Production event sender","environment":"production"}'
The response contains the project credential in data.token. Save it in a server-side secret manager immediately: the token is shown only when it is issued. Do not put it in browser JavaScript, a mobile app, source control, event payloads, or logs. If it is lost or exposed, revoke it and issue a replacement.
2. Send one event
Send JSON to POST /v1/events. Authenticate with the project credential as a bearer token:
export GIVEMORA_PROJECT_KEY='gm_project_...'
curl --fail-with-body --request POST 'https://givemora.com/v1/events' \
--header "Authorization: Bearer ${GIVEMORA_PROJECT_KEY}" \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: user-usr_123-signed-up-v1' \
--data '{
"id": "user-usr_123-signed-up-v1",
"type": "user.signed_up",
"occurred_at": "2026-09-24T18:30:00Z",
"severity": "info",
"data": {
"summary": "A new user created an account",
"user_id": "usr_123",
"registration_method": "email"
},
"actor": {"type": "user", "id": "usr_123"},
"tags": ["identity", "signup"],
"correlation_id": "request-abc-123"
}'
The body above is an example. Use identifiers and values from your application. The credential supplies the project and environment; the body supplies the event type, time, severity, and event data.
Accepted response
A valid event returns HTTP 202 Accepted, for example:
{
"accepted": true,
"event_id": "01...",
"duplicate": false,
"received_at": "2026-09-24T18:30:01+00:00",
"routing": "queued",
"routing_reason": null,
"routing_explain": null,
"request_id": "01..."
}
accepted: true means Givemora accepted and stored the event. It does not guarantee that a notification was delivered. Delivery is asynchronous. routing and routing_reason explain what happened to notification routing; see Routing results.
3. Send a batch
For multiple events, send a JSON array to POST /v1/events/batch. Every element is an event object with the same fields as a single event. Include a unique, stable id in every event so each item can be retried safely.
curl --fail-with-body --request POST 'https://givemora.com/v1/events/batch' \
--header "Authorization: Bearer ${GIVEMORA_PROJECT_KEY}" \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '[
{
"id":"invoice-inv_800-issued-v1",
"type":"billing.invoice_issued",
"severity":"success",
"data":{
"workspace_id":"ws_42",
"invoice_id":"inv_800",
"invoice_number":"800",
"amount_minor":1299,
"currency":"USD",
"summary":"Invoice 800 was issued"
}
},
{
"id":"invoice-inv_799-paid-v1",
"type":"billing.invoice_paid",
"severity":"success",
"data":{
"workspace_id":"ws_42",
"invoice_id":"inv_799",
"amount_minor":1299,
"currency":"USD"
}
}
]'
The batch response is HTTP 202 and contains an items array. Each result includes its original index; a valid item may be accepted even if another item fails validation or quota checks. Inspect each item rather than treating the batch-level accepted flag as proof that every event succeeded. The batch endpoint uses each event's id for deduplication; do not rely on an Idempotency-Key request header for a batch.
4. Event body reference
| Field | Requirement and rules |
|---|---|
id |
Optional string, 1–128 characters. Strongly recommended: create a stable unique ID for every logical event and reuse it for retries. |
type |
Required string, 1–100 characters. Must be lowercase canonical segments separated by dots, such as user.signed_up or orders.payment_failed. |
occurred_at |
Optional RFC 3339 timestamp that includes a timezone, such as 2026-09-24T18:30:00Z or 2026-09-24T22:00:00+03:30. |
severity |
Optional info, success, warning, or critical. If omitted, ingestion uses info; it does not automatically apply the catalog's recommended severity. |
data |
Optional JSON object for event-specific fields. data.summary may contain a human-readable summary of up to 500 characters. |
data.summary_translations |
Optional object with up to eight supported-language keys (en, fa, ar, es, fr, de, pt-BR, zh-CN) and a summary string of up to 500 characters for each. Notifications use the matching translation when present; otherwise they use data.summary as supplied. |
context |
Optional JSON object for application, request, deployment, or source context. |
actor |
Optional JSON object describing who or what initiated the action. |
subject |
Optional JSON object describing the entity affected by the action. |
tags |
Optional array of up to 20 distinct strings; each string must be 1–50 characters. |
correlation_id |
Optional string, 1–128 characters, useful for connecting related events across a workflow. |
data, context, actor, and subject must be JSON objects, not arrays. The complete body may be nested no deeper than 10 levels. Unknown top-level fields are rejected; put custom properties inside data, context, actor, or subject. Recognized sensitive key names are redacted during validation, but applications must not send secrets in the first place.
The event catalog's required_fields and optional_fields are the recommended contract for each standard event. Ingestion validates the common event envelope, but does not enforce those event-specific fields. Send the recommended required fields so that the event remains useful to your team and notification recipients.
5. Standard event catalog
The public catalog endpoint is GET /v1/event-catalog. It requires no credential and is limited to 60 requests per minute. It returns the versioned catalog, descriptions, categories, recommended severity, suggested fields, and privacy guidance. These names are recommended, not an allowlist: a project may send any type that follows the canonical syntax, unless that project's routing policy filters it.
| Event type | Meaning | Recommended severity | Recommended required data | Recommended optional data |
|---|---|---|---|---|
user.signed_up |
A user account was created. | info |
user_id |
registration_method, email_domain |
user.logged_in |
A user authenticated successfully. | info |
user_id |
method, ip_country |
user.logged_out |
A user signed out. | info |
user_id |
method |
user.email_verified |
A user verified their email address. | success |
user_id |
verification_method |
authenticator.code_sent |
A one-time or authenticator code was sent. Never send the code itself. | info |
user_id, channel |
attempt_id, expires_at |
authenticator.code_verified |
A one-time or authenticator code was verified. | success |
user_id |
attempt_id, channel |
authenticator.code_failed |
Code verification failed. | warning |
user_id |
attempt_id, channel, reason |
authenticator.mfa_enabled |
Multi-factor authentication was enabled. | success |
user_id |
method |
authenticator.mfa_disabled |
Multi-factor authentication was disabled. | warning |
user_id |
method, reason |
security.suspicious_login |
A suspicious sign-in was detected. | warning |
user_id |
reason, ip_country, method |
security.account_locked |
An account was locked. | warning |
user_id |
reason, lock_until, attempt_count |
security.password_reset_abuse |
Suspicious password-reset activity was detected. | warning |
user_id |
reason, ip_country, attempt_count |
security.api_key_created |
An API key was created. Never include its value. | info |
actor_user_id |
key_id, scope, workspace_id, project_id |
security.api_key_revoked |
An API key was revoked. Never include its value. | warning |
actor_user_id |
key_id, reason, workspace_id, project_id |
user.deleted |
A user account was deleted. | info |
user_id |
deletion_method, reason |
user.password_changed |
A user's password was changed. Never include a password or hash. | success |
user_id |
method |
user.password_reset_requested |
A password reset was requested. Never include reset tokens or codes. | info |
user_id |
channel, request_id, expires_at |
user.password_reset_completed |
A password reset was completed. Never include reset tokens or codes. | success |
user_id |
method, request_id |
user.invited |
A user was invited to an application or workspace. | info |
invited_user_id, inviter_user_id |
invitation_id, workspace_id, expires_at |
user.invitation_accepted |
A user accepted an invitation. | success |
user_id |
invitation_id, workspace_id |
session.created |
An authenticated session was created. Never include session tokens or cookies. | info |
user_id |
session_id, method, ip_country |
session.revoked |
An authenticated session was revoked. Never include session tokens or cookies. | info |
user_id |
session_id, reason, actor_user_id |
access.role_granted |
A role was granted to a user or service account. | info |
subject_user_id, role |
actor_user_id, workspace_id, project_id |
access.role_revoked |
A role was revoked from a user or service account. | warning |
subject_user_id, role |
actor_user_id, workspace_id, project_id |
access.permission_denied |
An authorization check denied an action. | warning |
user_id, permission |
resource_type, resource_id, reason |
project.created |
A project was created. | info |
project_id |
workspace_id, slug |
project.archived |
A project was archived. | info |
project_id |
workspace_id, reason |
integration.connected |
An external integration was connected. Never include credentials. | success |
integration |
provider, integration_id, workspace_id, project_id |
integration.disconnected |
An external integration was disconnected. | warning |
integration |
provider, integration_id, workspace_id, project_id, reason |
billing.invoice_issued |
An invoice was issued. | success |
workspace_id, invoice_id, invoice_number, amount_minor, currency |
plan, due_at |
billing.invoice_paid |
An invoice was paid. | success |
workspace_id, invoice_id, amount_minor, currency |
invoice_number, payment_id |
billing.invoice_overdue |
An invoice passed its due date unpaid. | warning |
workspace_id, invoice_id, due_at |
invoice_number, amount_minor, currency |
billing.subscription_started |
A subscription became active. | success |
workspace_id, plan |
subscription_id, billing_interval |
billing.subscription_canceled |
A subscription was canceled. | warning |
workspace_id |
subscription_id, plan, reason, canceled_at |
billing.payment_succeeded |
A payment completed successfully. | success |
workspace_id |
payment_id, currency, amount_minor |
billing.payment_failed |
A payment attempt failed. | warning |
workspace_id |
payment_id, reason |
billing.payment_refunded |
A payment was refunded. | warning |
workspace_id |
payment_id, amount_minor, currency, reason |
system.job_succeeded |
A background job completed. | success |
job |
job_id, duration_ms |
system.job_failed |
A background job failed after application-level handling. | critical |
job |
job_id, reason, attempt |
system.webhook_failed |
An inbound or outbound webhook failed processing. | warning |
webhook_id |
provider, reason, attempt |
6. Event intake options
| Request | Use it when | Authentication |
|---|---|---|
POST /v1/events |
Your application can construct Givemora's event JSON. Recommended for direct integrations. | Project bearer credential. Givemora HMAC headers are optional. |
POST /v1/events/batch |
Your application has several events ready to send together. | Project bearer credential. Givemora HMAC headers are optional. |
POST /v1/hooks/{source} |
A provider posts its native webhook body and you want Givemora to wrap it as an event. | Project bearer credential plus a valid signature. |
For a generic signed request, send all three headers: X-Givemora-Timestamp (Unix seconds), X-Givemora-Nonce (a unique printable value), and X-Givemora-Signature. Compute the signature over the exact request body bytes:
sha256=hex(HMAC-SHA256(project_credential, timestamp + "." + nonce + "." + raw_body))
The timestamp must be within five minutes of server time by default. A nonce cannot be reused with the same credential. If any Givemora signature header is sent, all three must be valid. Generic webhook ingestion at /v1/hooks/{source} always requires a signature. The ordinary /v1/events and batch endpoints accept bearer authentication without a request signature, unless signature headers are supplied.
For /v1/hooks/{source}, the source is a lowercase identifier of up to 32 characters. Givemora wraps the received JSON body under data and derives an event type from provider headers or the source name. Optional headers can set X-Givemora-Event-Type, X-Givemora-Event-Id, X-Givemora-Occurred-At, and X-Givemora-Severity. Native signature adapters exist for Stripe, GitHub/GitHub App, GitLab, Slack, and Shopify. The verifier uses the project credential as the signature secret; only use a native provider route when that provider can be configured to sign with this same secret. Otherwise, verify the provider signature in your application and forward a normalized event to POST /v1/events.
This Node.js example sends a custom provider-shaped JSON body through the generic signed webhook route. The body is signed exactly as sent; the X-Givemora-Event-* headers provide the canonical event envelope:
import { createHmac, randomUUID } from 'node:crypto';
const credential = process.env.GIVEMORA_PROJECT_KEY;
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = randomUUID();
const body = JSON.stringify({
user_id: 'usr_123',
summary: 'A new user created an account',
registration_method: 'email',
});
const signature = createHmac('sha256', credential)
.update(`${timestamp}.${nonce}.${body}`)
.digest('hex');
const response = await fetch('https://givemora.com/v1/hooks/my_app', {
method: 'POST',
headers: {
Authorization: `Bearer ${credential}`,
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Givemora-Timestamp': timestamp,
'X-Givemora-Nonce': nonce,
'X-Givemora-Signature': `sha256=${signature}`,
'X-Givemora-Event-Type': 'user.signed_up',
'X-Givemora-Event-Id': 'user-usr_123-signed-up-v1',
'X-Givemora-Severity': 'info',
},
body,
});
console.log(response.status, await response.json());
7. Routing results
Accepted events are stored even when notification routing is paused, muted, or filtered. A successful HTTP response means ingestion succeeded; check the response's routing fields to understand notification handling.
routing |
Meaning |
|---|---|
queued |
Notification deliveries were queued for active configured destinations and eligible outgoing webhooks. Actual delivery is asynchronous. |
paused |
The project has notifications paused. The event is still stored. |
muted |
A project event policy is currently muted. The event is still stored. |
filtered |
An event-type allowlist, severity threshold, rule, cooldown, or plan restriction suppressed routing. The event is still stored. |
heartbeat |
The event was recorded as a heartbeat signal; it is not a regular notification. |
synthetic |
The event is a synthetic check that is not being sent as a normal notification. |
Common routing_reason values include project_paused, muted_until, event_type_not_allowed, severity_below_threshold, rule_not_matched, cooldown_active, paid_channel_restricted, heartbeat_observed, and synthetic_check. A null reason generally means no suppression reason was recorded. The exact explanation, when available, is returned in routing_explain.
To inspect stored events, use a management token with events:read on GET /v1/manage/projects/{project_id}/events. That read endpoint is separate from the project credential; ingestion keys only write events.
8. Idempotency and safe retries
For a single event, send a stable unique id in the body. You may also send an Idempotency-Key header; if both are present, they must match. When the same project, environment, key, and body are sent again, Givemora returns 202 with duplicate: true without storing a second copy. Reusing a key with a different body returns 409 idempotency_conflict.
For batches, set an id on every item. Retry the complete batch after a timeout or transient error; Givemora deduplicates events that were already accepted. This is especially important if a service interruption occurs after some batch items have been processed.
Retry network failures and temporary 5xx errors, plus 429 rate-limit responses, with exponential backoff and jitter. Honor Retry-After when present. Do not repeatedly retry 400, 401, 403, or 422 errors without correcting the request. A 409 idempotency_conflict means the key was already used for a different event body; generate a new event ID only if it is a genuinely different logical event.
9. Limits and errors
The following are the application's default limits; deployments may override the byte and rate settings:
| Limit | Default |
|---|---|
| Single event JSON body | 256 KiB |
| Batch JSON body | 5 MiB |
| Events per batch | 100 |
| Maximum event nesting depth | 10 |
data.summary length |
500 characters |
| Translated summaries | 8 entries, each up to 500 characters |
| Tags | 20; each 1–50 characters |
| Event type | 1–100 characters |
| Per credential ingestion requests | 60 per minute |
| Per project ingestion requests | 120 per minute |
| Per workspace ingestion requests | 300 per minute |
The rate limits count HTTP requests; one batch request counts as one request, while each accepted event still counts toward billing and event usage quotas.
| HTTP status | Typical error codes | What to do |
|---|---|---|
400 |
malformed_json |
Send valid JSON. |
401 |
unauthenticated, signature_invalid |
Check the credential, expiry, revocation, and signature. |
403 |
insufficient_scope |
Use a project credential with events:write; management tokens cannot ingest. |
409 |
idempotency_conflict, signature_replay |
Use a unique event ID or a new signature nonce. |
413 |
payload_too_large |
Reduce the event or batch body size. |
422 |
validation_failed, idempotency_key_mismatch, batch_limit_exceeded |
Correct the body, header, type, timestamp, or batch size. |
429 |
rate_limit_exceeded, billing_quota_exceeded |
For rate limits, wait for Retry-After; for quota limits, check workspace usage and plan. |
503 |
ingestion_temporarily_unavailable |
Retry with backoff and the same event IDs. |
Error responses include an error.code, a request_id, and, when applicable, details. Log the request ID with your application's own event ID for support and diagnosis, but never log bearer credentials or signature secrets.
10. Security and data handling
Never send passwords, password hashes, one-time codes, reset tokens, session tokens, access or refresh tokens, API key values, provider credentials, secrets, authorization headers, or cookies. This is especially important for authenticator.code_sent: report that the code was sent and include only non-sensitive delivery metadata. Use opaque application identifiers instead of email addresses or other personal data when possible.
Givemora recognizes and redacts sensitive key names such as password, secret, token, access_token, refresh_token, api_key, authorization, and cookie, including nested values. Redaction is a defense in depth, not permission to submit sensitive data.
11. Quick checklist
- Create/select the project and issue a project credential for the right environment.
- Store the
gm_project_...credential in server-side secret storage. - Use a stable unique event
id, a canonical eventtype, and a timezone-awareoccurred_at. - Send the event to
POST https://givemora.com/v1/eventswithAuthorization: Bearer ...and JSON content type. - Check both the HTTP status and the returned
routing/routing_reason. - Retry temporary failures using the same event IDs; inspect each item in a batch response.
- Never put credentials, passwords, session tokens, or one-time codes in an event.