Edge API Reference
Version: 1.0.0
Base URL: https://edge.notifito.com
This document describes the public API endpoints exposed by the edge service.
Authentication
notifito uses two types of API keys:
| Key Type | Prefix | Location | Purpose |
|---|---|---|---|
| Publishable | pk_ |
Query parameter | Widget authentication |
| Secret | sk_ |
Bearer token | Push API, server-to-server |
Publishable Keys
Used for widget endpoints. Appear in page source by definition.
GET /v1/widget/schema?key=pk_live_xxx&item_type=sneaker
Secret Keys
Used for push API endpoints. Never exposed to browsers.
POST /v1/availability
Authorization: Bearer sk_live_xxx
Widget API
Get Widget Schema
Returns the attribute schema for rendering the subscription form.
Endpoint: GET /v1/widget/schema
Authentication: Publishable key
Rate Limit: 60 requests per minute per key
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | Publishable API key |
item_type |
string | Yes | Item type slug |
Response
200 OK
{
"tenant_name": "Sneaker Store",
"item_type": {
"id": "01a04f92-c67c-7093-88c8-28f1b313242c",
"name": "Sneaker",
"slug": "sneaker",
"schema_version": 1,
"attribute_schema": [
{
"key": "size",
"label": "Size",
"type": "enum",
"options": ["40", "41", "42", "43"],
"required": true,
"matchable": true,
"multiple": false
}
]
},
"items": [
{
"id": "01a04f92-c68e-7333-b731-c691115aed23",
"name": "Air Force 1",
"external_ref": "SKU-AF1"
}
]
}
403 Forbidden
{
"message": "Origin not allowed."
}
404 Not Found
{
"message": "Unknown item type."
}
Examples
cURL
curl "https://edge.notifito.com/v1/widget/schema?key=pk_live_xxx&item_type=sneaker"
JavaScript
const response = await fetch(
'https://edge.notifito.com/v1/widget/schema?key=pk_live_xxx&item_type=sneaker'
);
const schema = await response.json();
PHP
use Illuminate\Support\Facades\Http;
$response = Http::get('https://edge.notifito.com/v1/widget/schema', [
'key' => 'pk_live_xxx',
'item_type' => 'sneaker',
]);
$schema = $response->json();
Python
import requests
response = requests.get(
'https://edge.notifito.com/v1/widget/schema',
params={'key': 'pk_live_xxx', 'item_type': 'sneaker'}
)
schema = response.json()
Create Subscription
Submits a new subscription request. Triggers double opt-in flow.
Endpoint: POST /v1/widget/subscriptions
Authentication: Publishable key
Rate Limit: 30 requests per minute per key
Request Body
{
"key": "pk_live_xxx",
"item_type": "sneaker",
"item_id": "optional-item-uuid",
"attributes": {
"size": "42"
},
"contact_value": "consumer@example.com"
}
| Field | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | Publishable API key |
item_type |
string | Yes | Item type slug |
item_id |
string | No | Specific item UUID |
attributes |
object | No | Subscription attributes |
contact_value |
string | Yes | Email address |
Response
202 Accepted
{
"subscription_id": "sub-123",
"status": "pending_confirmation"
}
422 Unprocessable Entity
{
"message": "The given data was invalid.",
"errors": {
"attributes.size": ["Size must be one of: 40, 41, 42, 43."]
}
}
Examples
cURL
curl -X POST "https://edge.notifito.com/v1/widget/subscriptions" \
-H "Content-Type: application/json" \
-d '{
"key": "pk_live_xxx",
"item_type": "sneaker",
"attributes": {"size": "42"},
"contact_value": "consumer@example.com"
}'
JavaScript
const response = await fetch('https://edge.notifito.com/v1/widget/subscriptions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: 'pk_live_xxx',
item_type: 'sneaker',
attributes: { size: '42' },
contact_value: 'consumer@example.com'
})
});
const result = await response.json();
PHP
use Illuminate\Support\Facades\Http;
$response = Http::post('https://edge.notifito.com/v1/widget/subscriptions', [
'key' => 'pk_live_xxx',
'item_type' => 'sneaker',
'attributes' => ['size' => '42'],
'contact_value' => 'consumer@example.com',
]);
$result = $response->json();
Python
import requests
response = requests.post(
'https://edge.notifito.com/v1/widget/subscriptions',
json={
'key': 'pk_live_xxx',
'item_type': 'sneaker',
'attributes': {'size': '42'},
'contact_value': 'consumer@example.com'
}
)
result = response.json()
Push API
Push Availability
Signals that items are now available. Triggers matching and notification.
Endpoint: POST /v1/availability
Authentication: Secret key (Bearer token)
Rate Limit: 100 requests per minute per key
Headers
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Bearer sk_live_xxx |
Idempotency-Key |
Yes | Unique request ID for deduplication |
Content-Type |
Yes | application/json |
Request Body
{
"item_type": "sneaker",
"item_id": "optional-item-uuid",
"attributes": {
"size": "42"
},
"capacity": 3,
"available_from": "2026-09-01T00:00:00+00:00",
"available_until": "2026-09-08T00:00:00+00:00"
}
| Field | Type | Required | Description |
|---|---|---|---|
item_type |
string | Yes | Item type slug |
item_id |
string | No | Specific item UUID |
attributes |
object | No | Event attributes (wildcard if omitted) |
capacity |
integer | No | Available capacity |
available_from |
string | No | Availability window start (ISO 8601) |
available_until |
string | No | Availability window end (ISO 8601) |
Response
202 Accepted
{
"event_id": "evt-123",
"duplicate": false
}
202 Accepted (Duplicate)
{
"event_id": "evt-123",
"duplicate": true
}
422 Unprocessable Entity
{
"message": "The given data was invalid.",
"errors": {
"attributes.size": ["Size must be one of: 40, 41, 42, 43."]
}
}
Examples
cURL
curl -X POST "https://edge.notifito.com/v1/availability" \
-H "Authorization: Bearer sk_live_xxx" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"item_type": "sneaker",
"attributes": {"size": "42"},
"capacity": 3
}'
JavaScript
const response = await fetch('https://edge.notifito.com/v1/availability', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_xxx',
'Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json'
},
body: JSON.stringify({
item_type: 'sneaker',
attributes: { size: '42' },
capacity: 3
})
});
const result = await response.json();
PHP
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
$response = Http::withToken('sk_live_xxx')
->withHeaders(['Idempotency-Key' => Str::uuid()])
->post('https://edge.notifito.com/v1/availability', [
'item_type' => 'sneaker',
'attributes' => ['size' => '42'],
'capacity' => 3,
]);
$result = $response->json();
Python
import requests
import uuid
response = requests.post(
'https://edge.notifito.com/v1/availability',
headers={
'Authorization': 'Bearer sk_live_xxx',
'Idempotency-Key': str(uuid.uuid4()),
'Content-Type': 'application/json'
},
json={
'item_type': 'sneaker',
'attributes': {'size': '42'},
'capacity': 3
}
)
result = response.json()
Consent Pages
Confirm Subscription
Confirms a subscription via token link. No authentication required.
Endpoint: GET /c/{token}
Token Format: 32 alphanumeric characters
Response
200 OK — Confirmation page
404 Not Found — Invalid or expired token
410 Gone — Token already used
Unsubscribe
Unsubscribes via token link. No authentication required.
Endpoint: GET /u/{token} or POST /u/{token}
Token Format: 32 alphanumeric characters
Response
200 OK — Unsubscribe confirmation page
404 Not Found — Invalid token
Webhooks
Postmark Webhook
Receives delivery status updates from Postmark.
Endpoint: POST /webhooks/postmark
Authentication: HTTP Basic (Postmark credentials)
Request Body
Postmark webhook payload. See Postmark Documentation.
Response
200 OK — Webhook processed
Rate Limiting
| Endpoint | Limit | Window |
|---|---|---|
GET /v1/widget/schema |
60 requests | 1 minute |
POST /v1/widget/subscriptions |
30 requests | 1 minute |
POST /v1/availability |
100 requests | 1 minute |
Rate limits are applied per API key.
Rate Limit Headers
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1693000000
Rate Limit Exceeded
429 Too Many Requests
{
"message": "Too Many Requests."
}
CORS
Widget endpoints support Cross-Origin Resource Sharing.
Allowed Origins
Origins are checked against the tenant's registered domains:
{
"domains": ["shop.example.com", "www.shop.example.com"]
}
Subdomains are allowed if explicitly registered.
CORS Headers
Access-Control-Allow-Origin: https://shop.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, Idempotency-Key
Access-Control-Max-Age: 86400
Preflight Requests
Endpoint: OPTIONS /v1/widget/*
Returns CORS headers without requiring authentication.
Error Responses
All errors follow a consistent format:
{
"message": "Human-readable error message",
"errors": {
"field": ["Validation error message"]
}
}
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | Success |
| 202 | Accepted (async processing) |
| 401 | Unauthorized (invalid key) |
| 403 | Forbidden (origin not allowed) |
| 404 | Not Found |
| 422 | Validation Error |
| 429 | Rate Limit Exceeded |
| 500 | Internal Server Error |
Related Documentation
- OpenAPI Specification — Machine-readable API spec
- Internal API — Service-to-service API
- Attribute Types — The seven data types