One request per event
Each rule that matches an event sends one request. A 2xx reply is success. Timeout is 15 seconds.
How automation rules call your URL, what counts as a failure, when Oppy retries, and when it turns a rule off.
An automation rule says "when this happens, do that." Two of its actions call a web address you choose: Send to another app (a JSON webhook) and Ping Slack (a Slack incoming webhook).
Each rule that matches an event sends one request. A 2xx reply is success. Timeout is 15 seconds.
Only for temporary errors: timeouts, dropped connections, 429 and 5xx replies.
Permanent errors in a row, spread over at least 6 hours, turn the rule off until you turn it back on.
Every outbound call to an address you set comes from an automation rule. These are all of them.
| Action | What it sends | Auth and signing | Failures |
|---|---|---|---|
Send to another appsend_webhook | JSON to any public URL. POST by default, or PUT / PATCH. Default envelope or your own template. | API key header, HMAC-SHA256 signature, custom headers. | Recorded, retried and paused as described below. |
Ping Slacksend_notification | POST of {"text", "blocks"} to a Slack incoming-webhook URL. 10 second timeout. | None. The Slack URL is the secret. | Not checked. See Ping Slack. |
Built-in integration syncs, such as CRM updates, also call outside services. Oppy manages those addresses and credentials, so they are not covered here.
http:// or https:// and reach a public address. Private, loopback, link-local and cloud metadata addresses are blocked, including hostnames that resolve to them.301, 302 or 303 turns the request into a GET with no body. A redirect to another host drops your auth headers, and a 307 / 308 to another host is refused.With no template, Oppy sends this envelope with Content-Type: application/json:
{
"event": { "type": "contact.created", "fired_at": "2026-10-27T15:04:05Z" },
"rule": { "id": "8b0c…", "name": "New contacts to CRM" },
"payload": { "event_id": "3f2a…", "event_type": "contact.created", … }
}
payload.event_id names the Oppy event. Use it to drop duplicates. A retried delivery adds "automation_retry": true to payload, and a manual replay adds "automation_replay": true.
Set a body template to send your own JSON shape. Two markers fill in event fields:
{{name}} pastes the value as plain text.{{json:name}} writes the value as a JSON literal: strings quoted and escaped, numbers and booleans bare, missing fields as null. Use this form for every value inside a JSON body, and do not wrap it in quotes.{
"type": "call.summarized",
"contact": {{json:contact_name}},
"summary": {{json:summary}},
"seconds": {{json:duration_seconds}}
}
If the filled-in template is not valid JSON, Oppy sends {"message": "<the text>"} instead.
Set one or both on the rule. Both secrets are encrypted at rest.
| Option | Header Oppy sends | How to check it |
|---|---|---|
| API key | X-API-Key: <key>, or a header name you choose | Constant-time compare with your stored key. |
| Signing secret | X-Webhook-Signature: sha256=<hex> | HMAC-SHA256 of the raw request body with the secret, then a constant-time compare. |
| Custom headers | Any name and value. Values accept {{name}} markers. | Your choice. Host, Forwarded, X-Real-IP, Metadata-Flavor, X-Forwarded-* and Proxy-* are removed. |
The signature covers the exact bytes on the wire. Read the raw body before any JSON parsing.
# Python
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(header, expected)
// Node
const crypto = require("crypto");
function verify(rawBody, header, secret) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(header), b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Every delivery ends in one of three results. You can see each one in the rule's run history.
| Result | When | Retry | Counts toward pause |
|---|---|---|---|
| success | Any 2xx reply. | No | No. Resets the streak. |
| temporary failure | Timeout after 15 s, connection refused or reset, host unreachable, or a 429 or 5xx reply. The error starts with Temporary:. | Once, 5 minutes later | No |
| failure | Any other non-2xx reply, such as 400, 401, 403, 404 or 422. Also TLS certificate errors. | No | Yes |
| skipped | Conditions did not match, or the URL is missing, blocked, or its hostname does not resolve. | No | No. Does not break a streak either. |
automation_retry: true.Oppy does not queue events while your endpoint is down. After the one retry, a missed event stays missed until you replay it.
To recover: fix the receiver, turn the rule back on, then replay the events you missed. See Run history and replay.
200 or 202, and do slow work in the background. Oppy gives up after 15 seconds.5xx or 429 for problems that will pass, and 4xx only for requests that will never work. A 4xx, including 408, is never retried and moves the rule toward pausing.payload.event_id. A retry or replay sends the same event again.This action posts the rule's message to a Slack incoming webhook. It does not check Slack's reply.
If you need delivery tracking for Slack, use Send to another app pointed at the Slack webhook URL with a body template.
Every run is saved. For Send to another app, a run on your own rule also keeps a short record of the call. Credentials, header values and URL query values are replaced with [redacted].
| Field | What it holds |
|---|---|
status | success, failure or skipped |
error_message | Reason for a failure or skip. Temporary failures start with Temporary:. |
webhook_details.request | Method, URL with secrets removed, header names only, body size in bytes. |
webhook_details.response | Status code, content type, and the first 2 KB of your reply. |
Send your Oppy API key as Authorization: Bearer <key> or X-API-Key.
GET /api/automation_rules/:id/executions # runs for one rule
GET /api/automation_rules/executions/all # runs for the whole account
PATCH /api/automation_rules/:id # {"enabled": true} turns a paused rule back on
POST /api/automation_rules/replay # {"event_id": "…", "rule_id": "…"} sends a stored event again
Replay queues the event for the named rule, or for every enabled rule on that event type when you leave out rule_id. A paused rule cannot be replayed; turn it on first. Events created by automations cannot be replayed.
Any of these can trigger a rule. Add conditions on the event's fields to narrow it down, for example outcome is missed.
contact.createdcontact.updatedcontact.identifiedcontact.stage_changedcontact.score_changedcontact.dnc_addedconversation.createdconversation.status_changedconversation.needs_attention_setconversation.needs_attention_clearedconversation.tag_addedmessage.createdmessage.deliveredcall.endedcall.summarizedboard.createdboard.updatedlist.createdlist.updatedlist_item.createdlist_item.updatedlist_item.movedlist_item.deadline_changedlist_item.deadline_approachinglist_item.deadline_missednote.creatednote.updatedchannel.createdpage.viewedpage.navigatedpage.engagementvisitor.session_startvisitor.clickvisitor.link_clickvisitor.form_submitvisitor.scroll_milestonevisitor.idlevisitor.rage_clickwebsite_chat.conversation_startedwebsite_chat.message_sentwallet.balance_low_warningwallet.balance_low_urgentwallet.balance_criticalwallet.auto_recharge_failedorders.shipped.