Authentication & Webhooks#
Praemunio API · Version 1 · Developer DocumentationIntegrate securely with the Praemunio platform: authenticate with API keys, manage short-lived access tokens, control access with scopes, and receive real-time event notifications through signed webhooks. | |
|---|
| Base URL | https://api.praemunio.io/api/v1 |
| Format | JSON (Content-Type: application/json) |
| Authentication | API Key Pair → JWT Bearer token |
| Documentation | docs.praemunio.io |
Table of contents#
1. Overview#
The Praemunio API relies on two building blocks:Authentication
You exchange an API key pair (publicKey + secretKey) for a token pair:| Token | Type | Lifetime | Usage |
|---|
access_token | JWT | 15 minutes | Sent in the Authorization: Bearer <access_token> header on every request |
refresh_token | Opaque, single-use | 7 days | Exchanged for a new token pair (automatic rotation) |
Webhooks
Praemunio sends HTTP POST notifications to your endpoint when events occur on the platform (for example document.created, workspace.updated). Every delivery is signed with HMAC-SHA256, carries idempotency headers, is logged, and is automatically retried on failure.Authentication flow#
2. Quick start#
Step 2 — Call the API with your access tokenStep 3 — Renew tokens before they expireAlways store the new refresh_token. Each refresh token can be used only once.
3. API keys, scopes & rate limits#
3.1 Credentials#
| Credential | Format | Description |
|---|
publicKey | pk_live_… / pk_test_… | Public identifier of the key |
secretKey | sk_live_… / sk_test_… | Private secret. Shown only once at creation. Praemunio stores only a one-way hash and cannot recover it. |
pk_test_ / sk_test_ keys target the test environment; pk_live_ / sk_live_ keys target production.
Never expose a secretKey in client-side code, public repositories, or logs.
If a secret is lost or compromised, disable the key and create a new one from your dashboard.
Webhook secrets (whsec_…) are generated when a webhook is created, are stored encrypted, and are also shown only once. You use them to verify the signature of incoming deliveries (see section 7).3.2 Scopes & permissions#
Each API key carries granular permissions expressed as resource:action scopes.| Resource | Scopes |
|---|
| Documents | document:read, document:write, document:delete, document:share |
| Folders | folder:read, folder:write, folder:delete |
| Workspaces | workspace:read, workspace:manage |
| Teams | team:read, team:manage |
| Shares | share:read, share:write |
| Webhooks | webhook:manage |
A request made with a key that lacks the required scope is rejected with 403 Forbidden and the error code insufficient_scopes. Grant each key only the scopes it needs (principle of least privilege).3.3 Rate limits & quotas#
Each API key enforces two configurable ceilings:| Limit | Field | Default |
|---|
| Hourly rate limit | rateLimit | 1,000 requests / hour |
| Daily quota | dailyRequestLimit | 10,000 requests / day |
Rate limit information is returned on API responses:| Header | Description |
|---|
X-RateLimit-Limit | Maximum number of requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Time at which the window resets, as a Unix timestamp (seconds) |
When a limit is exceeded, the API returns 429 Too Many Requests with the error code rate_limit_exceeded. Wait until X-RateLimit-Reset before retrying.List endpoints (currently GET /webhooks and GET /webhooks/:id/logs) accept the following query parameters:| Parameter | Default | Maximum | Description |
|---|
page | 1 | — | Page number |
limit | 20 | 100 | Items per page |
GET /api/v1/webhooks?page=2&limit=50
4. Authentication endpoints#
All responses use the following envelope:4.1 Authenticate — POST /auth/api-key#
Exchanges an API key pair for an access token and a refresh token.| Field | Type | Required | Description |
|---|
publicKey | string | Yes | Your public key (pk_…) |
secretKey | string | Yes | Your secret key (sk_…) |
{
"publicKey": "pk_live_a1b2c3d4e5f67890",
"secretKey": "sk_live_9876543210fedcba9876543210fedcba"
}
{
"success": true,
"message": "Authentication successful",
"result": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "rt_8f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_expires_in": 604800
},
"statusCode": 200
}
| Field | Description |
|---|
token_type | Always Bearer |
expires_in | Access token lifetime, in seconds |
refresh_expires_in | Refresh token lifetime, in seconds |
Errors — 401 Unauthorizederrors.code | Message | Cause |
|---|
invalid_credentials | Invalid credentials | Unknown key or wrong secret |
api_key_disabled | API key is disabled | The key has been disabled |
api_key_expired | API key has expired | The key's expiration date has passed |
{
"success": false,
"message": "API key is disabled",
"result": null,
"errors": { "code": "api_key_disabled" },
"statusCode": 401
}
4.2 Refresh tokens — POST /auth/refresh-token#
Issues a new token pair from a valid refresh token. The submitted refresh token is consumed and can no longer be used.| Field | Type | Required | Description |
|---|
refresh_token | string | Yes | The current refresh token |
{
"refresh_token": "rt_8f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a..."
}
{
"success": true,
"message": "Token refreshed successfully",
"result": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "rt_9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_expires_in": 604800
},
"statusCode": 200
}
Errors — 401 Unauthorizederrors.code | Cause | Action |
|---|
invalid_grant | Refresh token invalid or revoked | Re-authenticate with your API key |
refresh_token_expired | Refresh token older than 7 days | Re-authenticate with your API key |
refresh_token_reuse_detected | A consumed refresh token was replayed outside the 30-second grace period; the whole session is revoked (see section 5) | Re-authenticate with your API key |
{
"success": false,
"message": "Refresh token reuse detected",
"result": null,
"errors": { "code": "refresh_token_reuse_detected" },
"statusCode": 401
}
4.3 Validate a key — POST /auth/validate-key#
Validates a raw secretKey and returns the key's metadata.| Field | Type | Required | Description |
|---|
key | string | Yes | The secret key to validate |
{
"key": "sk_live_9876543210fedcba9876543210fedcba"
}
{
"success": true,
"message": "API key is valid",
"result": {
"id": 42,
"publicId": "key_987654321",
"accountId": 10,
"name": "Production Service Key",
"publicKey": "pk_live_a1b2c3d4e5f67890",
"scopes": ["document:read", "document:write"],
"allowedOrigins": ["https://app.praemunio.io"],
"environment": "production",
"isActive": true,
"expiresAt": null,
"lastUsedAt": "2026-09-30T07:30:00.000Z",
"createdAt": "2026-01-15T10:00:00.000Z",
"updatedAt": "2026-09-30T07:30:00.000Z"
},
"statusCode": 200
}
| Field | Description |
|---|
scopes | Scopes granted to the key |
allowedOrigins | Origins allowed to use the key |
environment | production or test |
isActive | Whether the key is currently enabled |
expiresAt | Expiration date (ISO 8601), or null if the key does not expire |
lastUsedAt | Last time the key was used |
{
"success": false,
"message": "Invalid API key",
"result": null,
"errors": { "code": "invalid_api_key" },
"statusCode": 401
}
5. Token security model#
| Mechanism | Behavior |
|---|
| Refresh token rotation | Every refresh token is single-use. Each refresh returns a new access token and a new refresh token. |
| Grace period (30 s) | If a refresh request is retried within 30 seconds of the first one (for example after a network timeout), it is tolerated and the session is preserved. |
| Reuse detection | Replaying a consumed refresh token outside the grace period is treated as potential theft: all tokens of that session family are revoked immediately. Your client must re-authenticate with its API key. |
| Real-time key status | Disabling, deleting, or expiring an API key takes effect at the next token refresh. |
6. Webhooks#
Webhooks let you register HTTPS endpoints that receive real-time notifications about events in your account.6.1 Endpoints#
| Method | Endpoint | Description |
|---|
GET | /webhooks?page=1&limit=20 | List all webhooks (paginated) |
POST | /webhooks | Create a webhook (returns its secret once) |
GET | /webhooks/:id | Get webhook details |
PUT | /webhooks/:id | Update a webhook |
DELETE | /webhooks/:id | Delete a webhook (soft delete) |
POST | /webhooks/:id/test | Send a test delivery (webhook.test event) |
GET | /webhooks/:id/logs?page=1&limit=20 | Retrieve delivery logs (paginated) |
POST | /webhooks/:id/pause | Pause deliveries |
POST | /webhooks/:id/resume | Resume deliveries |
Webhook management requires the webhook:manage scope.6.2 Create a webhook — POST /webhooks#
| Field | Type | Required | Description |
|---|
name | string | Yes | Display name |
url | string | Yes | HTTPS endpoint receiving the events |
events | string[] | Yes | Events to subscribe to (see 6.4) |
environment | string | Yes | production or test |
description | string | No | Free-form description |
maxRetries | integer | No | Maximum number of retry attempts after a failed delivery |
retryInterval | integer | No | Delay between retries, in seconds (default: 10). The delay is fixed (no exponential backoff). |
{
"name": "Production Webhook",
"url": "https://example.com/api/webhooks",
"events": ["document.created", "document.updated", "workspace.storage_updated"],
"environment": "production",
"description": "Document event notifications",
"maxRetries": 3,
"retryInterval": 10
}
{
"success": true,
"message": "Webhook created successfully",
"result": {
"id": 12,
"publicId": "wh_a1b2c3d4-e5f6-7890-abcd-1234567890ab",
"name": "Production Webhook",
"url": "https://example.com/api/webhooks",
"events": ["document.created", "document.updated", "workspace.storage_updated"],
"secret": "whsec_3f8a9b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a",
"environment": "production",
"isActive": true,
"createdAt": "2026-09-30T07:00:00.000Z"
},
"statusCode": 201
}
Important: the secret is returned only once. Store it securely; you need it to verify signatures.
6.3 Delivery#
When a subscribed event occurs, Praemunio sends an HTTP POST request to your endpoint.| Header | Example | Description |
|---|
Content-Type | application/json | Body format |
X-Webhook-ID | wh_a1b2c3d4-e5f6-7890-abcd-1234… | Identifier of the registered webhook |
X-Webhook-Delivery-ID | del_9f8e7d6c-5b4a-3f2e-1d0c-9b8a7f… | Unique identifier of this delivery attempt (also available as deliveryId in the body) |
X-Webhook-Event | document.created | Event name |
X-Webhook-Signature | t=1700000000,v1=a1b2c3d4e5f6… | HMAC-SHA256 signature |
signed_payload = "<timestamp>.<raw_request_body>"
v1 = HEX( HMAC_SHA256(webhook_secret, signed_payload) )
Respond with any 2xx status as quickly as possible. Perform heavy processing asynchronously after acknowledging the request.A delivery is considered failed on a network error or an HTTP status >= 400.
Failed deliveries are retried up to maxRetries times, spaced by a fixed retryInterval (in seconds).
Every attempt is recorded (status, HTTP code, latency, response body) and available through GET /webhooks/:id/logs.
6.4 Event catalog#
| Category | Event | Triggered when |
|---|
| Documents | document.created | A document is uploaded or created |
| document.updated | A document's content or metadata is updated |
| document.deleted | A document is deleted |
| document.archived | A document is archived |
| document.restored | An archived document is restored |
| document.moved | A document is moved to another folder |
| document.shared | A document is shared via link or user invitation |
| Folders | folder.created | A folder is created |
| folder.updated | A folder is renamed or updated |
| folder.deleted | A folder is deleted |
| folder.moved | A folder is moved |
| Workspaces | workspace.created | A workspace is created |
| workspace.updated | A workspace is modified |
| workspace.deleted | A workspace is deleted |
| workspace.storage_updated | A workspace's storage quota is modified |
| Teams | team.created | A team is created |
| team.updated | A team is updated |
| team.deleted | A team is deleted |
| team.member_added | A member is added to a team |
| team.member_removed | A member is removed from a team |
| team.invitation_sent | A team invitation is sent |
| team.invitation_accepted | A team invitation is accepted |
| Shares | share.created | Share access is created |
| share.deleted | Share access is revoked |
| Usage | usage.quota_exceeded | An account quota or limit is exceeded |
| System | webhook.test | You trigger a test delivery via /webhooks/:id/test |
6.5 Event payload#
Every delivery has the same JSON envelope; the data object depends on the event.Example — document.created{
"id": "evt_9b1deb4d-3b7d-4bad-9bdd-2b0d7b3d0b1a",
"deliveryId": "del_a1b2c3d4-e5f6-7890-abcd-1234567890ab",
"event": "document.created",
"timestamp": "2026-09-30T08:00:00.000Z",
"data": {
"documentId": "doc_8849201",
"name": "Q3_Financial_Report.pdf",
"size": 2485910,
"mimeType": "application/pdf",
"folderId": "fol_1029",
"workspaceId": "ws_55",
"createdBy": {
"accountId": 42,
"email": "user@company.com"
}
}
}
6.6 Idempotency#
Because of retries, the same event may be delivered more than once. Your handler must be idempotent.Recommended deduplication flow:1.
Read the event identifier (id in the body, e.g. evt_...), which identifies the event itself.
2.
Look it up in a cache or database (for example Redis) holding the identifiers processed during the last 24 hours.
3.
If it was already processed, return 200 OK immediately without re-running your business logic.
4.
Otherwise process the event, then record the identifier.
X-Webhook-Delivery-ID / deliveryId identifies a single delivery attempt. Use it for logging and support requests, and to correlate with the entries returned by GET /webhooks/:id/logs.
7. Verifying webhook signatures#
Always verify the signature before processing a delivery. The check confirms the request comes from Praemunio and protects against replay attacks.1.
Parse X-Webhook-Signature to extract the timestamp (t) and the signature (v1).
2.
Reject the request if the timestamp is older than your tolerance window (300 seconds recommended).
3.
Recompute the HMAC-SHA256 of "<t>.<raw_body>" with your whsec_… secret.
4.
Compare with v1 using a constant-time comparison.
Use the raw request body. Re-serializing a parsed JSON object can change whitespace or key order and break the signature.
Node.js / TypeScript (Express)
8. Client integration example#
A TypeScript client that authenticates, refreshes tokens proactively, avoids concurrent refresh calls (refresh tokens are single-use, so parallel refreshes would trigger reuse detection), and respects rate limits.
9. Errors#
| HTTP status | errors.code | Cause | Recommended action |
|---|
200 OK | — | Success | Process the response |
201 Created | — | Resource created | Store any generated secret |
400 Bad Request | invalid_request | Malformed or invalid request | Fix the request parameters |
401 Unauthorized | invalid_credentials | Incorrect public key or secret key | Verify your credentials |
401 Unauthorized | invalid_api_key | Key rejected by /auth/validate-key | Verify the key |
401 Unauthorized | api_key_disabled | Key disabled by an administrator | Re-enable the key in the console |
401 Unauthorized | api_key_expired | Key expiration date passed | Generate a new key |
401 Unauthorized | invalid_grant | Refresh token invalid or revoked | Re-authenticate with your API key |
401 Unauthorized | refresh_token_expired | Refresh token expired (older than 7 days) | Re-authenticate with your API key |
401 Unauthorized | refresh_token_reuse_detected | Consumed refresh token replayed | Discard all tokens and re-authenticate |
403 Forbidden | insufficient_scopes | Required scope missing on the key | Add the required scopes to the key |
404 Not Found | not_found | Resource does not exist | Check the requested identifier |
429 Too Many Requests | rate_limit_exceeded | Hourly rate limit or daily quota exceeded | Wait until X-RateLimit-Reset |
500 Internal Server Error | internal_error | Unexpected server error | Retry later; contact support if it persists |
10. Security best practices#
Keep secrets server-side. Load secretKey and whsec_ values from environment variables or a secrets manager, never from source code.
Apply least privilege. Grant each key only the scopes it needs, and use separate keys per environment (test vs live) and per application.
Rotate keys regularly and disable any key you suspect is exposed.
Persist the latest refresh token atomically after every refresh; losing it forces a new authentication.
Serialize token refreshes in multi-threaded or multi-instance clients.
Respect rate limits and back off on 429 responses.
Verify every webhook signature and enforce the timestamp tolerance.
Make webhook handlers idempotent and answer quickly with a 2xx.
Use HTTPS endpoints only for webhooks.
Support#
Modified at 2026-09-30 08:07:59