Skip to main content

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

FieldTypeRequiredDescription
modestringNowebhook (default) or approval
namestringNoDisplay name (auto-generated if omitted)
descriptionstringNoOptional description (max 1000 chars)
timezonestringNoIANA timezone for scheduling (e.g., America/New_York). Defaults to UTC.
idempotency_keystringNoUnique key to prevent duplicate creation (max 255 chars)
callback_urlstringNoURL to receive lifecycle events (executed, failed, expired)

Schedule Fields

At least one scheduling field is required.

FieldTypeExampleDescription
schedule.presetstring"tomorrow"Named time preset
schedule.waitstring"2h"Relative wait duration from now
scheduled_forstring"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.

Sub-minute scheduling

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

FieldTypeRequiredDescription
requestobjectYesHTTP request configuration
request.urlstringYesDestination URL (HTTPS required in production)
request.methodstringNoHTTP method (default: POST)
request.headersobjectNoCustom headers as key-value pairs
request.bodyobjectNoJSON request body
max_attemptsintegerNoMaximum delivery attempts, 1-10 (default: 5)
retry_strategystringNoexponential (default) or linear
webhook_secretstringNoSecret used to sign the outgoing request

Approval Mode Fields

FieldTypeRequiredDescription
gateobjectYesApproval gate configuration
gate.messagestringYesMessage displayed to recipients (max 5000 chars)
gate.recipientsarrayYesEmail addresses, phone numbers (E.164), or contact IDs
gate.channelsarrayNoDelivery channels: email, sms, teams, slack, push. Defaults to ["email"].
gate.confirmation_modestringNofirst_response (default) or all_required
gate.max_snoozesintegerNoMaximum snooze count, 0-10 (default: 5)
gate.timeoutstringNoResponse timeout, e.g. 4h, 7d, 1w (default: 7d)
gate.on_timeoutstringNoWhat happens when the timeout expires: cancel (default), expire, or approve
gate.escalationobjectNoEscalation configuration
gate.escalation.contactsarrayNoEmail addresses or phone numbers for escalation
gate.escalation.after_hoursnumberNoHours before escalating (min: 0.5)
gate.attachmentsarrayNoFile attachments for the approval message
requestobjectNoOptional 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.

FieldTypeRequiredDescription
recurrence.frequencyintegerYesHow often to repeat (min: 1)
recurrence.unitstringYesTime unit: m (minutes), h (hours), d (days), w (weeks), M (months)
recurrence.end_typestringYesWhen to stop: never, count, or date
recurrence.max_occurrencesintegerConditionalRequired when end_type is count. Total executions (min: 2).
recurrence.end_datestringConditionalRequired 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.

FieldTypeRequiredDescription
dedup_keysarrayNoGrouping keys (max 10). Alphanumeric plus _, :, ., -.
coordinationobjectNoDedup behavior configuration
coordination.on_createstringNoskip_if_exists or replace_existing
coordination.on_executeobjectNoExecution-time conditions
coordination.on_execute.conditionstringNoskip_if_previous_pending, execute_if_previous_failed, execute_if_previous_succeeded, wait_for_previous
coordination.on_execute.on_condition_not_metstringNocancel (default), reschedule, fail
coordination.on_execute.reschedule_delayintegerNoSeconds before retry, 60-86400 (default: 300)
coordination.on_execute.max_reschedulesintegerNoMax 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 with meta.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

ParameterTypeDefaultDescription
statusstring--Filter by resolution status: pending_resolution, resolved, executing, awaiting_response, executed, failed, cancelled, expired
modestring--Filter by mode: immediate or gated
searchstring--Search in name and description
coordination_keystring--Filter by coordination key
recurringstring--Filter by recurrence: recurring or one-time
per_pageinteger25Results per page (max 100)
pageinteger1Page 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

FieldTypeDescription
idstringAction UUID
namestringDisplay name
modestringwebhook or approval
statusstringCurrent status
scheduled_forstringScheduled execution time (ISO 8601)
executed_atstringActual execution time (if completed)
requestobjectHTTP request config (webhook mode)
gateobjectApproval gate config (approval mode)
delivery_attemptsarrayHTTP delivery attempt log (webhook mode)
reminder_eventsarrayApproval event timeline (approval mode)
recipientsarrayRecipient list with response details (approval mode)
dedup_keysarrayDedup keys assigned to this action
is_recurringbooleanWhether this action repeats
recurrenceobjectRecurrence config (when recurring)
recurrence_countintegerNumber of completed executions (when recurring)
last_executed_atstringTimestamp of last execution (when recurring)
created_atstringCreation timestamp
updated_atstringLast update timestamp

Reminder Events Object

Each entry in the reminder_events array represents a lifecycle event for the approval.

FieldTypeDescription
idstringEvent UUID
event_typestringsent, confirmed, declined, snoozed, escalated, or expired
actor_emailstringEmail of the person who triggered the event (null for system events)
notesstringOptional comment left by the responder
created_atstringEvent timestamp (ISO 8601)

Recipients Object

Each entry in the recipients array represents one recipient and their response.

FieldTypeDescription
idstringRecipient UUID
emailstringRecipient email or phone number
statusstringpending, confirmed, declined, or snoozed
responded_atstringWhen the recipient responded (ISO 8601, null if pending)
response_commentstringOptional comment left by the recipient (max 500 chars, null if none)
display_namestringDisplay 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 failed status
  • 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"
]
}