Actions
Actions are the core primitive in CallMeLater. An action is either a scheduled HTTP request (webhook mode) or a human approval request (approval mode) that executes at a specified time.
Create Action
POST /actions
Common Fields
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | No | webhook (default) or approval |
name | string | No | Display name (auto-generated if omitted) |
description | string | No | Optional description (max 1000 chars) |
timezone | string | No | IANA timezone for scheduling (e.g., America/New_York). Defaults to UTC. |
idempotency_key | string | No | Unique key to prevent duplicate creation (max 255 chars) |
callback_url | string | No | URL to receive lifecycle events (executed, failed, expired) |
Schedule Fields
At least one scheduling field is required.
| Field | Type | Example | Description |
|---|---|---|---|
schedule.preset | string | "tomorrow" | Named time preset |
schedule.wait | string | "2h" | Relative wait duration from now |
scheduled_for | string | "2026-04-01T09:00:00Z" | Exact UTC timestamp (top-level field) |
Presets: tomorrow, next_week, next_monday through next_sunday, 1h, 2h, 4h, 1d, 3d, 1w
Wait format: Number followed by a unit -- s (seconds), m (minutes), h (hours), d (days), w (weeks). Examples: 30s, 5m, 2h, 14d, 1w.
Delays under 5 minutes are dispatched with second-level precision via a Redis queue, bypassing the minute-based scheduler. This requires a Redis queue connection and a worker processing the fast queue.
Webhook Mode Fields
| Field | Type | Required | Description |
|---|---|---|---|
request | object | Yes | HTTP request configuration |
request.url | string | Yes | Destination URL (HTTPS required in production) |
request.method | string | No | HTTP method (default: POST) |
request.headers | object | No | Custom headers as key-value pairs |
request.body | object | No | JSON request body |
max_attempts | integer | No | Maximum delivery attempts, 1-10 (default: 5) |
retry_strategy | string | No | exponential (default) or linear |
webhook_secret | string | No | Secret used to sign the outgoing request |
Approval Mode Fields
| Field | Type | Required | Description |
|---|---|---|---|
gate | object | Yes | Approval gate configuration |
gate.message | string | Yes | Message displayed to recipients (max 5000 chars) |
gate.recipients | array | Yes | Email addresses, phone numbers (E.164), or contact IDs |
gate.channels | array | No | Delivery channels: email, sms, teams, slack, push. Defaults to ["email"]. |
gate.confirmation_mode | string | No | first_response (default) or all_required |
gate.max_snoozes | integer | No | Maximum snooze count, 0-10 (default: 5) |
gate.timeout | string | No | Response timeout, e.g. 4h, 7d, 1w (default: 7d) |
gate.on_timeout | string | No | What happens when the timeout expires: cancel (default), expire, or approve |
gate.escalation | object | No | Escalation configuration |
gate.escalation.contacts | array | No | Email addresses or phone numbers for escalation |
gate.escalation.after_hours | number | No | Hours before escalating (min: 0.5) |
gate.attachments | array | No | File attachments for the approval message |
request | object | No | Optional HTTP request to execute after approval is granted |
Recurrence Fields
Make an action repeat on a schedule. After each successful execution, the action automatically re-schedules itself.
| Field | Type | Required | Description |
|---|---|---|---|
recurrence.frequency | integer | Yes | How often to repeat (min: 1) |
recurrence.unit | string | Yes | Time unit: m (minutes), h (hours), d (days), w (weeks), M (months) |
recurrence.end_type | string | Yes | When to stop: never, count, or date |
recurrence.max_occurrences | integer | Conditional | Required when end_type is count. Total executions (min: 2). |
recurrence.end_date | string | Conditional | Required when end_type is date. ISO 8601 timestamp, must be in the future. |
The minimum recurrence interval is 5 minutes.
You can also use repeat as an alias for recurrence.
Dedup Keys Fields
Group related actions and control their behavior with dedup keys.
| Field | Type | Required | Description |
|---|---|---|---|
dedup_keys | array | No | Grouping keys (max 10). Alphanumeric plus _, :, ., -. |
coordination | object | No | Dedup behavior configuration |
coordination.on_create | string | No | skip_if_exists or replace_existing |
coordination.on_execute | object | No | Execution-time conditions |
coordination.on_execute.condition | string | No | skip_if_previous_pending, execute_if_previous_failed, execute_if_previous_succeeded, wait_for_previous |
coordination.on_execute.on_condition_not_met | string | No | cancel (default), reschedule, fail |
coordination.on_execute.reschedule_delay | integer | No | Seconds before retry, 60-86400 (default: 300) |
coordination.on_execute.max_reschedules | integer | No | Max reschedule attempts, 1-100 (default: 10) |
on_create behaviors:
skip_if_exists-- Return the existing non-terminal action instead of creating a new one. The response uses status 200 withmeta.skipped: true.replace_existing-- Cancel all existing non-terminal actions sharing the same dedup keys, then create the new action.
on_execute conditions:
skip_if_previous_pending-- Cancel this action if another non-terminal action with the same key exists.execute_if_previous_failed-- Only execute if the most recent action with the same key failed.execute_if_previous_succeeded-- Only execute if the most recent action with the same key succeeded.wait_for_previous-- Reschedule until the previous action with the same key reaches a terminal state.
Example: Webhook Mode
curl -X POST https://callmelater.io/api/v1/actions \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"mode": "webhook",
"name": "Trial expiration webhook",
"idempotency_key": "trial-end-user-42",
"schedule": { "wait": "14d" },
"request": {
"method": "POST",
"url": "https://api.example.com/webhooks/trial-expired",
"headers": { "X-Custom-Header": "value" },
"body": { "event": "trial_expired", "user_id": 42 }
},
"max_attempts": 5,
"retry_strategy": "exponential"
}'
Example: Approval Mode
curl -X POST https://callmelater.io/api/v1/actions \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"mode": "approval",
"name": "Production deployment approval",
"idempotency_key": "deploy-approval-abc123",
"schedule": { "wait": "30m" },
"gate": {
"message": "Please approve the production deployment for release v2.1.0",
"recipients": ["tech-lead@example.com", "+15551234567"],
"channels": ["email", "sms"],
"timeout": "4h",
"max_snoozes": 3
},
"callback_url": "https://api.example.com/webhooks/approval-response"
}'
Example: Recurring Webhook
curl -X POST https://callmelater.io/api/v1/actions \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"mode": "webhook",
"name": "Hourly health check",
"schedule": { "wait": "5m" },
"request": {
"method": "POST",
"url": "https://api.example.com/health-check",
"body": { "source": "callmelater" }
},
"recurrence": {
"frequency": 1,
"unit": "h",
"end_type": "count",
"max_occurrences": 24
}
}'
Responses
201 Created
{
"data": {
"id": "01234567-89ab-cdef-0123-456789abcdef",
"name": "Trial expiration webhook",
"description": null,
"mode": "webhook",
"status": "scheduled",
"timezone": "UTC",
"scheduled_for": "2026-02-04T10:30:00Z",
"idempotency_key": "trial-end-user-42",
"dedup_keys": [],
"attempt_count": 0,
"max_attempts": 5,
"retry_strategy": "exponential",
"is_recurring": false,
"created_at": "2026-01-21T10:30:00Z",
"updated_at": "2026-01-21T10:30:00Z"
}
}
200 Skipped (when coordination.on_create is skip_if_exists and a matching action exists)
{
"data": {
"id": "existing-action-id",
"name": "Existing action",
"status": "scheduled"
},
"meta": {
"skipped": true,
"reason": "existing_action_found"
}
}
422 Validation Error
{
"message": "The given data was invalid.",
"errors": {
"request.url": ["The request.url field is required."]
}
}
List Actions
GET /actions
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | -- | Filter by resolution status: pending_resolution, resolved, executing, awaiting_response, executed, failed, cancelled, expired |
mode | string | -- | Filter by mode: immediate or gated |
search | string | -- | Search in name and description |
coordination_key | string | -- | Filter by coordination key |
recurring | string | -- | Filter by recurrence: recurring or one-time |
per_page | integer | 25 | Results per page (max 100) |
page | integer | 1 | Page number |
Response
{
"data": [
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"name": "Trial expiration webhook",
"mode": "webhook",
"status": "scheduled",
"scheduled_for": "2026-02-04T10:30:00Z",
"idempotency_key": "trial-end-user-42",
"dedup_keys": [],
"created_at": "2026-01-21T10:30:00Z",
"updated_at": "2026-01-21T10:30:00Z"
}
],
"meta": {
"current_page": 1,
"last_page": 5,
"total": 72
}
}
Get Action
GET /actions/{id}
Returns the full action detail including delivery history and approval events.
Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Action UUID |
name | string | Display name |
mode | string | webhook or approval |
status | string | Current status |
scheduled_for | string | Scheduled execution time (ISO 8601) |
executed_at | string | Actual execution time (if completed) |
request | object | HTTP request config (webhook mode) |
gate | object | Approval gate config (approval mode) |
delivery_attempts | array | HTTP delivery attempt log (webhook mode) |
reminder_events | array | Approval event timeline (approval mode) |
recipients | array | Recipient list with response details (approval mode) |
dedup_keys | array | Dedup keys assigned to this action |
is_recurring | boolean | Whether this action repeats |
recurrence | object | Recurrence config (when recurring) |
recurrence_count | integer | Number of completed executions (when recurring) |
last_executed_at | string | Timestamp of last execution (when recurring) |
created_at | string | Creation timestamp |
updated_at | string | Last update timestamp |
Reminder Events Object
Each entry in the reminder_events array represents a lifecycle event for the approval.
| Field | Type | Description |
|---|---|---|
id | string | Event UUID |
event_type | string | sent, confirmed, declined, snoozed, escalated, or expired |
actor_email | string | Email of the person who triggered the event (null for system events) |
notes | string | Optional comment left by the responder |
created_at | string | Event timestamp (ISO 8601) |
Recipients Object
Each entry in the recipients array represents one recipient and their response.
| Field | Type | Description |
|---|---|---|
id | string | Recipient UUID |
email | string | Recipient email or phone number |
status | string | pending, confirmed, declined, or snoozed |
responded_at | string | When the recipient responded (ISO 8601, null if pending) |
response_comment | string | Optional comment left by the recipient (max 500 chars, null if none) |
display_name | string | Display name (from contact or auto-detected) |
Example: Approval Response Detail
{
"data": {
"id": "01234567-89ab-cdef-0123-456789abcdef",
"name": "Validate legal answer",
"mode": "approval",
"status": "executed",
"scheduled_for": "2026-03-15T09:00:00Z",
"executed_at": "2026-03-15T09:12:34Z",
"recipients": [
{
"id": "recipient-uuid",
"email": "notaire@example.com",
"status": "confirmed",
"responded_at": "2026-03-15T09:12:34Z",
"response_comment": "Verified. This is correct per Article 1134 of the Civil Code.",
"display_name": "Me Dupont"
}
],
"reminder_events": [
{
"id": "event-uuid-1",
"event_type": "sent",
"actor_email": null,
"notes": null,
"created_at": "2026-03-15T09:00:00Z"
},
{
"id": "event-uuid-2",
"event_type": "confirmed",
"actor_email": "notaire@example.com",
"notes": "Verified. This is correct per Article 1134 of the Civil Code.",
"created_at": "2026-03-15T09:12:34Z"
}
]
}
}
Cancel Action
DELETE /actions/{id}
Cancel a pending action before it executes.
Cancellable statuses: scheduled, resolved, awaiting_response
Actions that have already reached a terminal status (executed, failed, expired) cannot be cancelled.
Response
200 OK
{
"data": {
"id": "01234567-89ab-cdef-0123-456789abcdef",
"name": "Trial expiration webhook",
"status": "cancelled"
}
}
Cancel by Idempotency Key
You can also cancel an action using its idempotency key instead of the UUID:
DELETE /actions
{
"idempotency_key": "trial-end-user-42"
}
Cancel by Coordination Key
Cancel all non-terminal actions sharing a coordination (dedup) key. This is useful for batch cancellation -- for example, cancelling everything related to a deployment or a user flow.
DELETE /actions/by-coordination-key
{
"coordination_key": "deploy:prod"
}
Actions in terminal states (executed, failed, cancelled, expired) are skipped. Only actions that are still pending, scheduled, or awaiting a response are cancelled.
200 OK
{
"message": "3 actions cancelled",
"cancelled": 3,
"skipped": 0,
"coordination_key": "deploy:prod"
}
404 Not Found -- No cancellable actions match the key (or the key doesn't exist in your account).
Retry Action
POST /actions/{id}/retry
Manually retry a failed action. This resets the attempt counter and creates a new execution cycle.
Requirements
- The action must be in
failedstatus - The action must have an HTTP request configuration (webhook mode)
Response
200 OK
{
"data": {
"id": "01234567-89ab-cdef-0123-456789abcdef",
"name": "Failed webhook",
"status": "resolved",
"attempt_count": 0,
"manual_retry_count": 1
}
}
422 Unprocessable Entity (if the action is not in failed status or has no HTTP config)
List Coordination Keys
GET /coordination-keys
Returns a list of all dedup keys used across your account. Use this to discover keys, then filter actions by a specific key using the dedup_key parameter on the List Actions endpoint.
Response
{
"keys": [
"deploy:api-service",
"deploy:web-app",
"migration:db-upgrade",
"user:42:trial"
]
}