Complete reference for the Apruvly REST API.
Everything you need to start automating approvals in minutes.
The Apruvly API is a RESTful JSON API. All requests must be authenticated with an API key. Responses are always JSON unless the response is empty (204).
https://api.apruvly.io/api/v1
Create a single-step approval workflow with one curl command:
curl -X POST https://api.apruvly.io/api/v1/workflow \
-H "Authorization: Bearer wf_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"object": {
"title": "Q1 Marketing Budget",
"description": "Marketing budget for Q1, $18k total"
},
"expires": "48h",
"start": "step1",
"workflow": {
"step1": {
"type": "email",
"data": {
"to": ["[email protected]"],
"subject": "Q1 Marketing Budget – $18k",
"body": "Please review the budget breakdown in the linked document and share your decision."
}
}
}
}'
All API requests require authentication via an API key.
Pass your API key in the Authorization header as a Bearer token. You can create and manage API keys from the dashboard.
Authorization: Bearer wf_YOUR_API_KEY
You can generate an API key from the API Keys section in the dashboard.
/api/v1/workflow
Creates a new approval workflow and immediately starts the first step.
| Field | Type | Required | Description |
|---|---|---|---|
object |
object | Yes | Metadata about the item being approved. |
object.title |
string | Yes | Human-readable title for the workflow. Max 200 characters. |
object.description |
string | No | Optional longer description of what is being approved. |
object.links |
string[] | No | Optional list of URLs related to the approval (documents, files, etc.). |
object.tags |
string[] | No | Optional list of tags for filtering and organization. |
external_id |
string | No | Optional caller-supplied business correlation id (max 128 chars) — e.g. contract number, CRM ticket, order reference. Requires Growth plan or higher. Must be unique per account. |
expires |
duration | Yes | How long the workflow stays open before expiring. Format: 30s, 15m, 2h, 7d, 2w. |
start |
string | Yes | The key of the first step to execute (must match a key in workflow). |
workflow |
map[string]Step | Yes | Map of step keys to Step objects defining the approval chain. |
on |
map[string]Action[] | No | Map of lifecycle events to lists of actions (webhook, email, twilio-sms) fired automatically. Events: workflow_created, workflow_approved, workflow_rejected, workflow_expired, workflow_canceled, workflow_completed, step_approved, step_escalated, user_approval, user_rejection. |
disable_email_fallback |
boolean | No | Opts out of the default email fallback. When omitted (recommended), the platform automatically delivers the approval request via email if an integration notification (Teams, Slack, WhatsApp) fails. Set to true to skip the fallback and accept the original failure. |
debug |
boolean | No |
Enables debug mode. All notifications (email, Teams, Slack, WhatsApp), webhooks, and escalations are suppressed — the workflow runs through its full logic without contacting any participants. Useful for testing the flow without triggering real communications.
Even in debug mode, credits are consumed normally. Debug mode only suppresses external communications — it does not affect billing.
|
curl -X POST https://api.apruvly.io/api/v1/workflow \
-H "Authorization: Bearer wf_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"object": {
"title": "Contract Sign-off",
"description": "Legal review for client contract #4821",
"links": ["https://drive.example.com/contracts/4821"],
"tags": ["legal", "contract"]
},
"external_id": "CONTRACT-2025-0042",
"expires": "72h",
"start": "legal-review",
"workflow": {
"legal-review": {
"type": "email",
"data": {
"to": ["[email protected]"],
"subject": "Approval Request: Contract #4821",
"body": "Please review and approve contract #4821."
},
"approved": "ceo-approval",
"escalation": {
"type": "email",
"after": "24h",
"data": {
"to": ["[email protected]"],
"subject": "Escalation: Contract #4821 pending approval",
"body": "Contract #4821 has not been reviewed. Please take action."
}
}
},
"ceo-approval": {
"type": "email",
"data": {
"to": ["[email protected]"],
"subject": "Final Approval: Contract #4821",
"body": "Legal review is complete. Please provide final approval."
}
}
}
}'
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"externalId": "CONTRACT-2025-0042"
}
}
Attach your own business reference (contract number, CRM ticket, deploy id) when creating a workflow, then poll status or cancel without storing the platform UUID. Requires the Growth plan or higher. Max 128 characters; allowed: letters, digits, and . _ : / -. Unique per account.
{
"success": false,
"message": "Your plan does not include external_id. Upgrade to Growth or higher to attach a business correlation id to workflows."
}
curl -X POST https://api.apruvly.io/api/v1/workflow \
-H "Authorization: Bearer wf_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"object": {
"title": "Contract Sign-off",
"description": "Legal review for client contract #4821"
},
"external_id": "CONTRACT-2025-0042",
"expires": "72h",
"start": "legal-review",
"workflow": {
"legal-review": {
"type": "email",
"data": {
"to": ["[email protected]"],
"subject": "Approval Request: Contract #4821",
"body": "Please review and approve contract #4821."
}
}
}
}'
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"externalId": "CONTRACT-2025-0042"
}
}
Returns the same payload as GET /workflow/{id}, looked up by the external_id supplied at creation. Requires Growth plan or higher.
curl https://api.apruvly.io/api/v1/workflow/external/CONTRACT-2025-0042 \
-H "Authorization: Bearer wf_YOUR_API_KEY"
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"externalId": "CONTRACT-2025-0042",
"status": "pending",
"currentStep": "legal-review",
"createdAt": "2025-01-15T10:00:00Z",
"expiresAt": "2025-01-18T10:00:00Z",
"decisions": []
}
}
curl -X DELETE https://api.apruvly.io/api/v1/workflow/external/CONTRACT-2025-0042 \
-H "Authorization: Bearer wf_YOUR_API_KEY"
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000"
}
}
curl "https://api.apruvly.io/api/v1/workflows/search?external_id=CONTRACT-2025-0042" \
-H "Authorization: Bearer wf_YOUR_API_KEY"
{
"success": true,
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Contract Sign-off",
"external_id": "CONTRACT-2025-0042",
"current_status": "pending",
"current_step": "legal-review",
"created_at": "2025-01-15T10:00:00Z",
"expires_at": "2025-01-18T10:00:00Z",
"completed_at": null
}
]
}
/api/v1/workflow/validate
Validates a workflow configuration without creating it. Useful for checking your payload before submitting.
curl -X POST https://api.apruvly.io/api/v1/workflow/validate \
-H "Authorization: Bearer wf_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"object": {
"title": "Test Workflow"
},
"expires": "24h",
"start": "step1",
"workflow": {
"step1": {
"type": "email",
"data": {
"to": ["[email protected]"],
"subject": "Test Approval",
"body": "Please approve this test request."
}
}
}
}'
{
"success": true
}
/api/v1/workflow/{id}
Retrieves the full state of a workflow including its activity log and decision history.
| Field | Type | Description |
|---|---|---|
id |
uuid | The workflow ID returned when the workflow was created. |
curl https://api.apruvly.io/api/v1/workflow/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer wf_YOUR_API_KEY"
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"externalId": "CONTRACT-2025-0042",
"status": "pending",
"currentStep": "ceo-approval",
"currentEscalation": 0,
"createdAt": "2025-01-15T10:00:00Z",
"expiresAt": "2025-01-18T10:00:00Z",
"nextTickAt": "2025-01-16T10:00:00Z",
"decisions": [
{
"user": "[email protected]",
"is_approved": true,
"ip": "203.0.113.42",
"decisionAt": "2025-01-15T14:22:00Z"
}
]
}
}
/api/v1/workflows/search
Returns a paginated list of workflows belonging to the authenticated user.
| Field | Type | Description |
|---|---|---|
status |
string | Filter by status: pending, approved, rejected, expired, canceled |
query |
string | Full-text search on workflow title. |
step |
string | Filter by current active step name (e.g. legal-review). |
from |
RFC3339 | Filter workflows created on or after this timestamp (RFC 3339 format, e.g. 2025-01-01T00:00:00Z). |
to |
RFC3339 | Filter workflows created on or before this timestamp (RFC 3339 format, e.g. 2025-01-31T23:59:59Z). |
tags |
string | Filter by workflow tags (comma-separated). |
external_id |
string | Exact match on the caller-supplied business correlation id. Requires Growth plan or higher. |
limit |
integer | Maximum number of results to return (default 50, max 500). Max results per page (default 50, max 500). |
curl "https://api.apruvly.io/api/v1/workflows/search?status=pending&query=contract" \
-H "Authorization: Bearer wf_YOUR_API_KEY"
{
"success": true,
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Contract Sign-off",
"external_id": "CONTRACT-2025-0042",
"current_status": "pending",
"current_step": "ceo-approval",
"created_at": "2025-01-15T10:00:00Z",
"expires_at": "2025-01-18T10:00:00Z",
"completed_at": null
}
]
}
/api/v1/workflows/recent
Returns the most recently created workflows for the authenticated user.
curl "https://api.apruvly.io/api/v1/workflows/recent" \
-H "Authorization: Bearer wf_YOUR_API_KEY"
/api/v1/billing
Returns the authenticated user's subscription, plan, credit balance and usage summary.
curl "https://api.apruvly.io/api/v1/billing" \
-H "Authorization: Bearer wf_YOUR_API_KEY"
/api/v1/integrations
Manage third-party channel credentials (Slack, MS Teams, Twilio, etc.) via the REST API.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/integrations | List configuration status for every supported integration. |
| GET | /api/v1/integrations/{integration} | Return configuration status for one integration (secrets are never included). |
| PUT | /api/v1/integrations/{integration} | Create or replace credentials for an integration. |
| DELETE | /api/v1/integrations/{integration} | Remove stored credentials for an integration. |
/api/v1/workflow/{id}
Cancels a pending workflow. Completed or expired workflows cannot be canceled.
You can also cancel by the business id you set at creation time — see Business correlation IDs (external_id). Business correlation IDs (external_id)
| Field | Type | Description |
|---|---|---|
id |
uuid | The workflow ID returned when the workflow was created. |
curl -X DELETE https://api.apruvly.io/api/v1/workflow/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer wf_YOUR_API_KEY"
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000"
}
}
/api/v1/workflow/{id}/{challenge}/approve
or
/api/v1/workflow/{id}/{challenge}/reject
Records an approval or rejection decision for a workflow step. The action (approve or reject) is determined by the URL path suffix, not the request body. No request body is required.
| Field | Type | Description |
|---|---|---|
id |
uuid | The workflow ID returned when the workflow was created. |
challenge |
uuid | The per-approver UUID challenge token included in the notification email link. |
# Approve
curl -X PUT https://api.apruvly.io/api/v1/workflow/550e8400-e29b-41d4-a716-446655440000/7a9f1c2d-4b3e-48f6-9d8c-1a2b3c4d5e6f/approve \
-H "Authorization: Bearer wf_YOUR_API_KEY"
# Reject
curl -X PUT https://api.apruvly.io/api/v1/workflow/550e8400-e29b-41d4-a716-446655440000/7a9f1c2d-4b3e-48f6-9d8c-1a2b3c4d5e6f/reject \
-H "Authorization: Bearer wf_YOUR_API_KEY"
{
"success": true
}
/api/v1/directory
Manage your address book of areas (teams) and people. Contacts feed email fallback at notification time and link recipients for cross-channel analytics.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/directory/areas | List every active area. |
| POST | /api/v1/directory/areas | Create a new area. |
| POST | /api/v1/directory/areas/bulk | Create up to 100 areas in a single call. |
| PUT | /api/v1/directory/areas/bulk | Rename many areas at once. Each item must include the area id. |
| GET | /api/v1/directory/areas/{id} | Fetch one area by id. |
| PUT | /api/v1/directory/areas/{id} | Rename an area. |
| DELETE | /api/v1/directory/areas/{id} | Delete an area (memberships are cascaded; people are kept). |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/directory/people?q=&area=&page= | Paginated list of people. Filters: q (text), area (uuid), page (1-based). |
| POST | /api/v1/directory/people | Create a person with memberships and contacts. |
| POST | /api/v1/directory/people/bulk | Create up to 1,000 people in a single call (with their memberships and contacts). |
| PUT | /api/v1/directory/people/bulk | Update many people at once (name, memberships and contacts). Each item must include the person id. |
| GET | /api/v1/directory/people/{id} | Fetch one person with full memberships and contacts. |
| PUT | /api/v1/directory/people/{id} | Replace display name, area memberships and contacts in one call. |
| DELETE | /api/v1/directory/people/{id} | Delete the person (contacts and memberships are cascaded). |
curl -X POST https://api.apruvly.io/api/v1/directory/people \
-H "Authorization: Bearer wf_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"display_name": "Ana Souza",
"area_ids": ["6c2b6e80-1f74-4cf2-9e2f-0d0e3b9b0001"],
"contacts": [
{ "provider": "email", "address": "[email protected]", "label": "work" },
{ "provider": "twilio-sms", "address": "+5511999999999" }
]
}'
{
"success": true,
"data": {
"id": "a4d1f0c6-2d24-49b3-9e3c-7a2e6c4b0001",
"display_name": "Ana Souza",
"is_active": true,
"area_ids": ["6c2b6e80-1f74-4cf2-9e2f-0d0e3b9b0001"],
"contacts": [
{ "id": "…", "provider": "email", "address": "[email protected]", "label": "work" },
{ "id": "…", "provider": "twilio-sms", "address": "+5511999999999" }
]
}
}
Bulk endpoints accept an array of items under a single "items" field. Creation is capped per request (100 areas, 1,000 people) and updates are limited to twice the customer's plan size for the relevant resource.
curl -X POST https://api.apruvly.io/api/v1/directory/people/bulk \
-H "Authorization: Bearer wf_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"display_name": "Ana Souza",
"area_ids": ["6c2b6e80-1f74-4cf2-9e2f-0d0e3b9b0001"],
"contacts": [
{ "provider": "email", "address": "[email protected]", "label": "work" }
]
},
{
"display_name": "Bruno Lima",
"contacts": [
{ "provider": "twilio-sms", "address": "+5511988887777" }
]
}
]
}'
{
"success": true,
"data": {
"count": 2,
"items": [
{ "id": "a4d1f0c6-…", "display_name": "Ana Souza", "area_ids": ["6c2b6e80-…"], "contacts": ["…"] },
{ "id": "f10b2e8a-…", "display_name": "Bruno Lima", "area_ids": [], "contacts": ["…"] }
]
}
}
The whole batch is processed together: if any single entry is invalid the request is rejected and nothing is saved.
Each endpoint requires the matching API key scope. Grant scopes under API Keys in the console.
| Scopes | Description |
|---|---|
api:directory:list | API · Directory · list |
api:directory:read | API · Directory · read |
api:directory:write | API · Directory · create / update |
api:directory:bulk | API · Directory · bulk operations |
api:directory:delete | API · Directory · delete |
Each contact is a (provider, address) pair. Only providers configured under /integrations (plus email) are accepted.
| Provider | Address |
|---|---|
email, slack, ms-teams | E-mail address |
twilio-sms, twilio-whatsapp, whatsapp | E.164 phone (e.g. +5511999999999) |
telegram | Numeric chat id |
discord | 17–20 digit snowflake |
DingTalk group robots are not stored in the directory — they broadcast to a group, not an individual contact.
| Status | Description |
|---|---|
| 400 | Missing required field or invalid contact address (e.g. malformed e-mail or phone). |
| 402 | The current plan does not include the directory feature. |
| 409 | Directory feature is disabled — enable it under /directory in the web console first. |
Side effects the engine fires when workflow lifecycle events occur.
Use the `on` field to register actions for workflow lifecycle events. Each action has a type (webhook, email, or twilio-sms) and a data payload. When an event fires, all actions are executed in order.
Each action is a JSON object that can be serialized in two forms. The wrapped form ({"type":"...", "data":{...}}) is the canonical form produced by the API and the only one accepted for twilio-sms. The flat form infers the type from the payload fields (url → webhook, to → email) and is kept for backward compatibility.
{
"on": {
"workflow_approved": [
{ "type": "webhook", "data": { "url": "https://example.com/hooks/approved", "method": "POST" } },
{ "type": "email", "data": { "to": ["[email protected]"], "subject": "Approved", "body": "Workflow ${id} is approved." } },
{ "type": "twilio-sms", "data": { "to": ["+14155552671"], "body": "Workflow ${id} approved" } }
]
}
}
| Event | Description |
|---|---|
workflow_created |
Fires when a workflow is successfully created. |
workflow_approved |
Fires when the entire workflow reaches an approved final state. |
workflow_rejected |
Fires when the entire workflow is rejected. |
workflow_expired |
Fires when the workflow expires before reaching a final decision. |
workflow_canceled |
Fires when the workflow is explicitly canceled via the API. |
workflow_completed |
Fires when the workflow reaches any terminal state (approved, rejected, expired, or canceled). |
step_approved |
Fires when an individual step is approved and the workflow advances to the next step. |
step_escalated |
Fires when a step's escalation timer triggers and the escalation notification is sent. |
user_approval |
Fires each time a single user casts an approval vote on a step. |
user_rejection |
Fires each time a single user casts a rejection vote on a step. |
WebhookAction
— type: "webhook"
Sends an HTTP request to an external endpoint. Private and loopback hosts are rejected at dispatch time.
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | Must be "webhook". |
url |
string | Yes | The destination URL. Must be a valid URI. |
method |
string | No | HTTP method: GET, POST, PUT, PATCH, or DELETE. Defaults to POST. |
headers |
object | No | Custom HTTP headers sent with the request. Useful for authentication tokens or API keys. |
body |
object|string | No | Request body. Supports variable interpolation with ${variable} syntax. |
timeout |
duration | No | Request timeout duration (e.g. "30s", "5m"). Defaults to server configuration. |
async |
boolean | No | If true, the request is sent asynchronously and does not block the workflow. |
skipCertVerify |
boolean | No | If true, skips TLS certificate verification. Use only in development environments. |
EmailAction
— type: "email"
Sends an email through your custom SMTP (when configured) or the system postman.
| Field | Type | Required | Description |
|---|---|---|---|
to |
string[] | Yes | Recipient addresses. At least one is required. |
subject |
string | No | Email subject line. |
body |
string | No | Email body. Supports ${variable} interpolation. |
cc |
string[] | No | Optional list of CC recipients. |
bcc |
string[] | No | Optional list of BCC recipients. |
html |
boolean | No | If true, the body is sent as HTML; otherwise as plain text. |
SmsAction
— type: "twilio-sms"
Sends a fire-and-forget SMS through Twilio. Informational only — no short-code correlation or reply tracking.
| Field | Type | Required | Description |
|---|---|---|---|
to |
string[] | Yes | Phone numbers in E.164 format (e.g. +14155552671). Duplicates and invalid entries are rejected. |
body |
string | Yes | Message text (up to 1600 characters). Supports ${variable} interpolation. |
You can use the following variables inside webhook body values. They are automatically replaced at execution time.
| Variable | Description |
|---|---|
${id} |
The unique workflow UUID. |
${status} |
Current workflow status (e.g. approved, rejected, expired). |
${current_step} |
The identifier of the currently active step. |
${escalation_level} |
Current escalation depth level (0 if no escalation has occurred). |
{
"on": {
"workflow_approved": [
{
"type": "webhook",
"data": {
"url": "https://api.yourapp.com/webhooks/approved",
"method": "POST",
"headers": { "Authorization": "Bearer your-token" },
"body": {
"workflow_id": "${id}",
"status": "${status}",
"current_step": "${current_step}"
},
"timeout": "30s"
}
}
],
"workflow_rejected": [
{
"type": "webhook",
"data": {
"url": "https://api.yourapp.com/webhooks/rejected",
"async": true
}
}
],
"step_approved": [
{
"type": "webhook",
"data": {
"url": "https://api.yourapp.com/webhooks/step-done",
"body": {
"step": "${current_step}"
}
}
}
]
}
}
In addition to workflow-level events, each step can register its own actions that fire when THAT step is approved or rejected — independent of the final workflow outcome. Use `step.actions.approved` and `step.actions.rejected` to trigger webhooks or emails per decision.
{
"workflow": {
"review": {
"type": "email",
"data": {
"to": ["[email protected]"],
"subject": "Please review",
"body": "Approve or reject ${id}"
},
"actions": {
"approved": [
{
"type": "webhook",
"data": {
"url": "https://example.com/hooks/review-approved",
"method": "POST"
}
}
],
"rejected": [
{
"type": "email",
"data": {
"to": ["[email protected]"],
"subject": "Review rejected",
"body": "${id} was rejected"
}
}
]
}
}
}
}
Cost: each webhook counts as 1 credit; each recipient in email or twilio-sms actions counts as 1 credit per recipient. CC and BCC on email actions are not charged.
Each step costs 1 credit plus 1 per configured escalation level. External delivery actions add a single flat surcharge shared between webhook and twilio-sms.
| Type | Cost |
|---|---|
webhook |
+1 per workflow (regardless of how many webhooks or events). |
email |
No additional cost. |
twilio-sms |
+1 per workflow (shared with webhook — max +1 total). |
twilio-whatsapp |
+1 per workflow (shared with webhook — max +1 total). |
discord |
+1 per workflow (regardless of how many webhooks or events). |
Credits are debited once at workflow creation time. There is no per-dispatch charge.
Detailed description of all request and response objects.
Step| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | Notification type. Currently supported: email, ms-teams, slack, whatsapp, telegram, dingtalk, discord, twilio-sms, twilio-whatsapp |
data |
EmailData | MSTeamsData | SlackData | WhatsAppData | TelegramData | DingTalkData | DiscordData | TwilioSMSData | TwilioWhatsAppData | Yes | Notification payload. Shape depends on type. |
minApprovals |
integer | No | Minimum number of approvals needed to advance. Default: 1. |
minRejections |
integer | No | Minimum number of rejections needed to reject the workflow. Default: 1. |
approved |
string | No | Key of the step to execute when this one is approved. Omit to end the workflow with approval status. |
rejected |
string | No | Key of the step to execute when this one is rejected. Omit to end the workflow with rejected status. |
escalation |
Escalation | No | Optional escalation config to trigger if approvers do not respond in time. |
actions |
{approved, rejected} | No | Optional actions triggered when THIS step is approved or rejected, independent of the final workflow outcome. See the Actions section for supported shapes. (Per-step actions) |
Channel delivery
Shared channel delivery posts one approval message to a team channel, group, or chat. Only users listed in allowList can click the interactive Approve/Reject buttons; the decision log records the clicker, not the channel ID.
| Type | Description |
|---|---|
slack |
to: channel ID (C…). allowList: Slack user IDs (U…) and/or workspace emails. Requires Signing Secret. |
ms-teams |
to: teamId/channelId. allowList: Azure AD object IDs and/or emails. Channel posts use a Microsoft Graph protected API — your tenant must grant Resource-Specific Consent (e.g. ChannelMessage.Send) on the Teams app or an admin-approved application permission. The Apruvly bot must be installed in the target team. |
telegram |
to: negative group/channel chat ID (-100…). allowList: Telegram user IDs. Bot must be a group member. |
discord |
to: channel snowflake (17–20 digits). allowList: Discord user snowflakes. Requires Interactions URL and Public Key. |
dingtalk |
DingTalk always broadcasts to the robot group — delivery: "channel" is rejected. |
CardOverride| Field | Type | Required | Description |
|---|---|---|---|
title |
string | No | Optional title override for the notification card or embed. |
description |
string | No | Optional text shown in the notification body or embed description. |
links |
string[] | No | Optional URLs rendered as action buttons (e.g. view request). |
EmailData — used when type = email
| Field | Type | Required | Description |
|---|---|---|---|
to |
string[] | Yes | List of approver email addresses for this step. |
subject |
string | Yes | Subject line of the approval email. |
body |
string | Yes | Plain-text or HTML message to the approver. object.title and object.description appear automatically in a summary card; approve/reject buttons are added for you. |
cc |
string[] | No | Optional list of CC recipients. |
bcc |
string[] | No | Optional list of BCC recipients. |
isHtml |
boolean | No | Set to true if body contains HTML markup. Default: false. |
These placeholders can be used in the subject and body fields.
| Variable | Description |
|---|---|
${title} |
Workflow object title (also shown in the summary card). |
${description} |
Workflow object description (also shown in the summary card). |
${id} |
The unique workflow UUID. |
${user_email} |
Recipient email address. |
${user_name} |
Recipient display name, or the local part of the email when no display name is set. |
SlackData — used when type = slack
| Field | Type | Required | Description |
|---|---|---|---|
delivery |
string | No | Delivery mode: omit or "direct" (default) sends one message per recipient; "channel" posts a single message to a shared destination with an allow-list of deciders. Supported on slack, ms-teams, telegram, and discord. |
allowList |
string[] | No | When delivery is "channel", list of emails and/or provider-native user IDs authorized to click Approve/Reject. Required in channel mode. |
to |
string[] | Yes | Direct mode: workspace email addresses (one DM per recipient). Channel mode: Slack channel IDs starting with C. |
card |
CardOverride | No | Optional per-step override for the notification title, description, and action links. When omitted, the workflow-level object fields are used. |
MSTeamsData — used when type = ms-teams
| Field | Type | Required | Description |
|---|---|---|---|
delivery |
string | No | Delivery mode: omit or "direct" (default) sends one message per recipient; "channel" posts a single message to a shared destination with an allow-list of deciders. Supported on slack, ms-teams, telegram, and discord. |
allowList |
string[] | No | When delivery is "channel", list of emails and/or provider-native user IDs authorized to click Approve/Reject. Required in channel mode. |
to |
string[] | Yes | Direct mode: Azure AD email addresses. Channel mode: teamId/channelId pairs (one message per entry). |
card |
CardOverride | No | Optional per-step override for the notification title, description, and action links. When omitted, the workflow-level object fields are used. |
WhatsAppData — used when type = whatsapp
| Field | Type | Required | Description |
|---|---|---|---|
to |
string[] | Yes | List of approver phone numbers in E.164 format (e.g. +5511999999999). |
card |
CardOverride | No | Optional per-step override for the notification title, description, and action links. When omitted, the workflow-level object fields are used. |
TelegramData — used when type = telegram
| Field | Type | Required | Description |
|---|---|---|---|
delivery |
string | No | Delivery mode: omit or "direct" (default) sends one message per recipient; "channel" posts a single message to a shared destination with an allow-list of deciders. Supported on slack, ms-teams, telegram, and discord. |
allowList |
string[] | No | When delivery is "channel", list of emails and/or provider-native user IDs authorized to click Approve/Reject. Required in channel mode. |
to |
string[] | Yes | Direct mode: positive user chat IDs (e.g. 123456789). Channel mode: negative group or channel chat IDs (e.g. -1001234567890). |
markdown |
boolean | No | When true, the message is sent using Telegram's MarkdownV2 format instead of HTML. The description field can contain MarkdownV2 syntax (e.g. *bold*, _italic_, `code`). Default: false. |
card |
CardOverride | No | Optional per-step override for the notification title, description, and action links. When omitted, the workflow-level object fields are used. |
DiscordData — used when type = discord
| Field | Type | Required | Description |
|---|---|---|---|
delivery |
string | No | Delivery mode: omit or "direct" (default) sends one message per recipient; "channel" posts a single message to a shared destination with an allow-list of deciders. Supported on slack, ms-teams, telegram, and discord. |
allowList |
string[] | No | When delivery is "channel", list of emails and/or provider-native user IDs authorized to click Approve/Reject. Required in channel mode. |
to |
string[] | Yes | Direct mode: Discord user snowflakes (17–20 digits). Channel mode: channel snowflake. The bot must share a server with DM recipients. |
card |
CardOverride | No | Optional per-step override for the notification title, description, and action links. When omitted, the workflow-level object fields are used. |
DingTalkData — used when type = dingtalk
| Field | Type | Required | Description |
|---|---|---|---|
to |
string[] | No | Optional mobile numbers (E.164) to @mention in the group ActionCard. DingTalk always posts to the configured robot group. |
card |
CardOverride | No | Optional per-step override for the notification title, description, and action links. When omitted, the workflow-level object fields are used. |
TwilioSMSData — used when type = twilio-sms
| Field | Type | Required | Description |
|---|---|---|---|
to |
string[] | Yes | Enter phone numbers in E.164 format (e.g. +5511999999999). Recipients must have consented to receive SMS. |
card |
CardOverride | No | Optional per-step override for the notification title, description, and action links. When omitted, the workflow-level object fields are used. |
TwilioWhatsAppData — used when type = twilio-whatsapp
| Field | Type | Required | Description |
|---|---|---|---|
to |
string[] | Yes | Phone number of the WhatsApp Sender registered with Twilio, in E.164 format (without the whatsapp: prefix). |
card |
CardOverride | No | Optional per-step override for the notification title, description, and action links. When omitted, the workflow-level object fields are used. |
Escalation| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | Notification type for the escalation. Currently supported: email, ms-teams, slack, whatsapp, telegram, dingtalk, discord, twilio-sms, twilio-whatsapp |
after |
duration | Yes | How long to wait before triggering the escalation. Minimum: 5m. |
data |
EmailData | TelegramData | … | Yes | Notification payload sent to the escalation recipients. |
escalation |
Escalation | No | Optional nested escalation to trigger if this escalation level is also not responded to. Maximum depth: 5. |
Decision — returned inside decisions[] in GET /workflow/{id}| Field | Type | Description |
|---|---|---|
user |
string | Email address of the approver who submitted the decision. |
is_approved |
boolean | true if approved, false if rejected. |
ip |
string | IP address from which the decision was submitted. |
decisionAt |
RFC3339 | RFC 3339 timestamp of when the decision was recorded. |
Business and Professional plans can send approval emails through your own SMTP server instead of the platform mailer. Configure it under Settings → Integrations → Custom SMTP (not in the workflow JSON).
Step keys (used in start, approved and rejected fields, and as workflow map keys) must match the following pattern - lowercase letter followed by lowercase letters, digits, hyphens or underscores:
^[a-z][a-z0-9_-]+$
Durations are expressed as a number followed by a unit suffix:
30s — 30 seconds
15m — 15 minutes
2h — 2 hours
7d — 7 days
2w — 2 weeks
Workflow expiration must be between 5 minutes (minimum) and 7 days (maximum).
The API uses standard HTTP status codes. Error responses include a JSON body with error and code fields.
| Status Code | Meaning |
|---|---|
| 200 | Request succeeded. |
| 202 | Accepted - workflow was created and is now running. |
| 400 | Bad Request - the request body or parameters are invalid. |
| 401 | Unauthorized - missing or invalid API key. |
| 402 | Payment Required - no active subscription, insufficient credits, or the current plan does not include the requested feature. |
| 403 | Forbidden - the API key lacks permission for this resource (e.g. you are not the workflow owner). |
| 404 | Not Found - the workflow or resource does not exist. |
| 406 | Not Acceptable - the operation cannot be performed in the current workflow state (e.g. cancelling an already-completed workflow). |
| 429 | Too Many Requests - rate limit exceeded. |
| 500 | Internal Server Error - something went wrong on our side. |
{
"success": false,
"message": "Invalid workflow ID.",
"data": {
"field": "id",
"value": "not-a-valid-uuid"
}
}
Common use cases with friendly descriptions to help you get started quickly.
The most basic use case: a single person needs to approve something before you proceed. Ideal for expense approvals, content sign-offs, or any single-gatekeeper scenario.
Route an approval through multiple people in sequence - Legal → Manager → CEO. Each step must be approved before the next begins. Perfect for contract sign-offs or policy approvals.
If the assigned approver does not respond within a set time window, automatically notify a fallback contact. Keeps processes moving without manual follow-ups.
Require a minimum number of approvals from a group - any N out of M members can approve. Useful for board decisions, committee approvals, or peer reviews.
Automatically POST to your backend when a workflow reaches a final decision. Trigger downstream automation without polling the API.
Check that your workflow configuration is valid before creating it. No authentication needed, no credits consumed - just a quick syntax and logic check.
Route a new hire approval through HR, the hiring manager, and IT provisioning in sequence. Each team only receives the request after the previous step is complete.
A single-step PTO request sent to the direct manager. If no response in 24 hours, HR is automatically notified. Webhook fires when the decision is made.
Gate a production release behind three sequential approvals: tech lead code review, QA sign-off, and ops authorization. A webhook triggers the CI/CD pipeline on approval.
Two-level approval chain (manager then CFO) each with its own escalation path. If the manager goes silent, the VP steps in; if the CFO delays, the CEO is notified. Webhooks handle downstream finance automation.
Post a single approval message to a shared Slack channel. Configure delivery: "channel", the channel ID in to, and an allowList of users authorized to click Approve/Reject.
JSON-RPC 2.0 endpoint for AI agents and automation pipelines
The MCP endpoint follows the Model Context Protocol specification using JSON-RPC 2.0 over HTTP. It allows AI agents and automation tools to pause execution and request human approval before taking critical actions.
POST https://mcp.apruvly.io/mcp
Authorization: Bearer wf_YOUR_API_KEY
Content-Type: application/json
MCP-aware clients can fetch a public discovery document at GET /.well-known/mcp to auto-detect the endpoint URL, supported transports and protocol versions, the auth scheme, and the catalog of available tools. No authentication required.
GET https://mcp.apruvly.io/.well-known/mcp
{
"endpoint": "https://mcp.apruvly.io/mcp",
"transports": ["streamable-http"],
"protocolVersions": ["2025-03-26"],
"auth": { "scheme": "bearer" },
"tools": ["create_approval_request", "get_approval_status", "cancel_approval_request"]
}
Your AI agent calls create_approval_request with a title, description and the approvers' emails.
Apruvly sends an approval email to each approver containing approve/reject links.
The agent polls get_approval_status until the status changes to approved, rejected, or expired.
Configuration examples for popular AI tools
Add the following MCP server entry to your project's .mcp.json file or global settings:
{
"mcpServers": {
"apruvly": {
"type": "streamable-http",
"url": "https://mcp.apruvly.io/mcp",
"headers": {
"Authorization": "Bearer wf_YOUR_API_KEY"
}
}
}
}
Add the following to the mcpServers section in your Claude Desktop configuration file:
{
"mcpServers": {
"apruvly": {
"url": "https://mcp.apruvly.io/mcp",
"headers": {
"Authorization": "Bearer wf_YOUR_API_KEY"
}
}
}
}
Add the MCP server in Cursor settings under Features > MCP Servers, or add it to .cursor/mcp.json in your project:
{
"mcpServers": {
"apruvly": {
"url": "https://mcp.apruvly.io/mcp",
"headers": {
"Authorization": "Bearer wf_YOUR_API_KEY"
}
}
}
}
Add the following MCP server entry to your project's .mcp.json file or global settings:
// .vscode/mcp.json
{
"servers": {
"apruvly": {
"type": "http",
"url": "https://mcp.apruvly.io/mcp",
"headers": {
"Authorization": "Bearer wf_YOUR_API_KEY"
}
}
}
}
Any MCP-compatible client can connect to the Apruvly endpoint using Streamable HTTP transport. Send JSON-RPC 2.0 requests directly:
curl -X POST https://mcp.apruvly.io/mcp \
-H "Authorization: Bearer wf_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"clientInfo": {"name": "my-agent", "version": "1.0"},
"capabilities": {}
}
}'
Available tools that AI agents can invoke via tools/call
create_approval_requestCreates a human approval request and returns a workflow_id that can be used to poll for the decision. The notification is sent over the channel chosen in `channel` (default email); non-email channels must already be configured for the account.
| Field | Type | Description | |
|---|---|---|---|
title |
string | Required | Short title (max 200 characters) describing what requires approval. |
description |
string | Optional | Detailed description of the action requiring human approval (max 4000 characters). |
channel |
string | Optional | Delivery channel. Defaults to "email". Allowed: email, slack, ms-teams, whatsapp, telegram, dingtalk, discord, twilio-sms, twilio-whatsapp. Non-email channels require the matching integration to be configured at /integrations. |
recipients |
array | Optional | Uniform recipient list, one entry per approver. Use this to target any channel. The address format depends on the channel (email/slack/ms-teams = email address, whatsapp/twilio-sms/twilio-whatsapp = E.164 phone, telegram = numeric chat ID, discord = 17-20 digit user snowflake). Mutually exclusive with approvers and steps. |
approvers |
array | Optional | Approvers (comma-separated emails) — {"email":"…","name":"…"}. Email-only shortcut; for other channels use recipients. |
steps |
array | Optional | Optional sequential approval chain. Each step must be approved before the next runs; any rejection terminates the workflow. Requires the Growth, Business or Professional plan. Mutually exclusive with top-level recipients/approvers. |
expires |
string | Optional | Duration string: "1h", "24h", "7d". Defaults to "24h". Minimum: 5 minutes — shorter values are rejected before any credit is debited. |
links |
array | Optional | List of related URLs (documents, files, links). |
tags |
array | Optional | List of labels for filtering and categorization. |
idempotency_key |
string | Optional | Optional client-supplied identifier (max 128 chars) that makes the call idempotent. Replaying the same key within 24h returns the original workflow_id without recreating a workflow or charging credits. Equivalent to the Idempotency-Key HTTP header. |
callback_url |
string | Optional | Optional https URL that receives a signed POST when the workflow reaches a terminal state. Useful for batch agents that cannot keep a polling or SSE connection open. |
external_id |
string | Optional | Optional caller-supplied correlation id (max 128 chars) — e.g. agent run_id, upstream tool_call_id, internal ticket id. Requires Growth plan or higher. Persisted on the workflow row and echoed back from get_approval_status. |
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "create_approval_request",
"arguments": {
"title": "Deploy to production",
"description": "Requesting approval to deploy v2.5.0 to production.",
"channel": "slack",
"recipients": [
{ "address": "[email protected]" }
],
"expires": "24h",
"idempotency_key": "agent-run-2026-05-03-00042",
"external_id": "deploy-pipeline-7831"
}
}
}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{ "type": "text", "text": "Approval request created…" },
{ "type": "text", "text": "{\"workflow_id\":\"550e8400-…\",\"status\":\"pending\",\"expires_at\":\"2026-05-04T12:00:00Z\",\"external_id\":\"deploy-pipeline-7831\"}" }
],
"structuredContent": {
"workflow_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"expires_at": "2026-05-04T12:00:00Z",
"external_id": "deploy-pipeline-7831"
}
}
}
Tool responses also carry a structuredContent field that mirrors the JSON snapshot in the second content item — declared by the tool's outputSchema so MCP-aware clients can parse it tipado without scanning the text payload.
A two-step chain: the manager has 24h to approve before the request escalates to the director, and only after the manager approves does the VP get the second-level request.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "create_approval_request",
"arguments": {
"title": "Vendor Contract – Novatek",
"channel": "email",
"steps": [
{
"name": "manager",
"recipients": [{ "address": "[email protected]" }],
"escalate_after": "24h",
"escalate_to": [{ "address": "[email protected]" }]
},
{
"name": "vp",
"recipients": [{ "address": "[email protected]" }]
}
],
"expires": "7d"
}
}
}
get_approval_statusReturns the current status and step decisions of an approval request. Poll this after create_approval_request until the status changes to approved, rejected, or expired.
| Field | Type | Description | |
|---|---|---|---|
workflow_id |
string | Required | The workflow ID returned when the workflow was created. |
wait |
string | Optional | Optional long-poll duration (e.g. "30s", "60s", "1m"). When set, the call blocks until the workflow leaves the pending state or the duration elapses, whichever comes first. Capped at 60s. Reduces the number of polling round-trips. |
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_approval_status",
"arguments": {
"workflow_id": "550e8400-e29b-41d4-a716-446655440000",
"wait": "60s"
}
}
}
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{ "type": "text", "text": "Approval status retrieved." },
{ "type": "text", "text": "{\"workflow_id\":\"550e8400-…\",\"status\":\"approved\",\"current_step\":\"approval\",\"external_id\":\"deploy-pipeline-7831\",\"decisions\":[…]}" }
],
"structuredContent": {
"workflow_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "approved",
"current_step": "approval",
"created_at": "2026-05-03T11:00:00Z",
"expires_at": "2026-05-04T12:00:00Z",
"external_id": "deploy-pipeline-7831",
"decisions": [{ "user": "[email protected]", "is_approved": true, "decided_at": "2026-05-03T11:42:18Z", "step": "manager" }]
}
}
}
Each decision entry may include an optional step field naming the workflow step at decision time. Legacy single-step workflows may omit it.
cancel_approval_requestCancels a pending approval request. Stops reminders, notifies approvers via every configured channel that the request was canceled, and marks the workflow as terminally canceled. Idempotent: a second call on an already-canceled workflow returns success with already_canceled=true.
| Field | Type | Description | |
|---|---|---|---|
workflow_id |
string | Required | The workflow ID returned when the workflow was created. |
reason |
string | Optional | Optional free-text reason for the cancellation (max 500 chars). Recorded on the workflow activity log so the customer can audit why the agent withdrew the request. |
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "cancel_approval_request",
"arguments": {
"workflow_id": "550e8400-e29b-41d4-a716-446655440000",
"reason": "agent retry resolved the action without human approval"
}
}
}
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{ "type": "text", "text": "Approval request canceled." },
{ "type": "text", "text": "{\"workflow_id\":\"550e8400-…\",\"status\":\"canceled\",\"already_canceled\":false,\"canceled_at\":\"2026-05-03T12:34:56Z\"}" }
],
"structuredContent": {
"workflow_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "canceled",
"already_canceled": false,
"canceled_at": "2026-05-03T12:34:56Z"
}
}
}
JSON-RPC 2.0 methods, handshake, and error codes
| Method | Description |
|---|---|
initialize |
Initialize an MCP session. Negotiates protocol version (echoes the client's version when supported, downgrades to the highest supported version below it, or returns -32602 with the supported list when no compatible version exists) and returns server info plus capabilities. |
ping |
Health check. Returns an empty result object. |
tools/list |
Returns the list of available tools, their JSON Schema input definitions, and an outputSchema describing the structuredContent each tool returns. |
tools/call |
Invokes a tool by name with the provided arguments. |
notifications/* |
Methods starting with notifications/ are accepted with HTTP 202 and no response body, as required by the spec. notifications/cancelled is honored to abort an in-flight tools/call (see "Notifications" below); the rest are silently discarded. |
initialize{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"clientInfo": { "name": "my-agent", "version": "1.0" },
"capabilities": {}
}
}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"serverInfo": { "name": "apruvly", "version": "1.0.0" },
"capabilities": { "tools": {} },
"instructions": "Apruvly is a human-in-the-loop approval service…"
}
}
The endpoint accepts a JSON array of requests in the body (max 50 items) and returns an array of responses. Items are processed sequentially; get_approval_status wait is capped at 5s inside a batch. notifications/cancelled entries in the same array are applied before any tools/call runs. Notifications produce no response slot; an array containing only notifications returns 202 Accepted with an empty body. An empty array is rejected with -32600 Invalid Request.
[
{ "jsonrpc": "2.0", "id": 1, "method": "ping" },
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "get_approval_status",
"arguments": { "workflow_id": "550e8400-e29b-41d4-a716-446655440000" } } }
]
[
{ "jsonrpc": "2.0", "id": 1, "result": {} },
{ "jsonrpc": "2.0", "id": 2, "result": { "content": [ … ], "structuredContent": { … } } }
]
notifications/cancelled aborts an in-flight tools/call so the agent can shorten a long-poll without waiting for its timer to fire. Match the requestId field to the id of the request you want to cancel; the running call returns its current snapshot immediately. Inside a batch, include the notification in the same array — it is honored before tools/call items run. To abort a long-poll already in flight on a separate connection, send notifications/cancelled as a sibling POST. Cancelling a request id that is not currently in flight is a silent no-op.
notifications/cancelled{
"jsonrpc": "2.0",
"method": "notifications/cancelled",
"params": {
"requestId": 2,
"reason": "agent changed its mind"
}
}
Response 202 Accepted — empty body, per the JSON-RPC notification convention. The matching tools/call wakes from its long-poll and returns its current snapshot to the original caller.
Application-level errors use code -32000 with a stable slug under data.code so agents can branch programmatically without parsing message text.
| Code | Meaning |
|---|---|
-32700 |
Parse error — invalid JSON body. |
-32600 |
Invalid Request — missing jsonrpc or method. |
-32601 |
Method not found. |
-32602 |
Invalid params — missing or malformed arguments. Also returned by initialize when the client requests a protocol version older than anything the server supports (the response carries data.supported with the list of acceptable versions). |
-32603 |
Internal error. |
-32000 |
Application error — branch on the stable slug under data.code (see table below). |
data.code slugs (application errors)data.code |
Meaning | extra data fields |
|---|---|---|
subscription_required |
The caller has no active subscription. | — |
plan_feature_required |
The current plan does not include the requested feature (MCP access, multi-step approval, …). | feature |
integration_not_configured |
The chosen channel needs an integration that the tenant has not configured at /integrations. |
channel, integration |
trial_expired |
Trial period has expired; upgrade to continue. | — |
approver_limit_exceeded |
The workflow lists more approvers than the plan allows. | max |
escalation_limit_exceeded |
The escalation chain is deeper than the plan allows. | max |
concurrent_workflow_limit |
The plan's cap on simultaneously-pending workflows was reached. | max |
rate_limited |
Too many requests in the current window. Retry after retry_after_seconds. |
window, retry_after_seconds |
insufficient_credits |
Account balance cannot cover the cost of the workflow. | — |
workflow_not_found |
The workflow_id does not exist for the calling account. Cross-tenant lookups intentionally return the same slug to avoid leaking existence. |
— |
workflow_not_cancelable |
cancel_approval_request was called on a workflow already in a non-pending terminal state (approved, rejected, expired, error). |
status |
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32000,
"message": "Rate limit exceeded, try again in a moment",
"data": {
"code": "rate_limited",
"window": "minute",
"retry_after_seconds": 60
}
}
}