Inbound webhooks
Let an external system start a workflow by posting to a stable, signed URL
What inbound webhooks do
An inbound webhook gives a use case a stable public URL that an outside system can POST to in order to start an execution. GitHub, Stripe, a form tool, a monitoring system, or any service that can send an HTTP request becomes a trigger for your workflow.
It is the fifth trigger source, alongside manual, chat, schedule and API. See Triggers and execution.
Inbound and outbound webhooks point in opposite directions, and they are separate features.
An inbound webhook lets an external system start a workflow. An outbound webhook lets MagOneAI tell an external system that a run finished. They share a name and nothing else: different permissions, different configuration, different tables.
You can use both together for a full round trip. An outbound webhook can even filter on the inbound_webhook trigger type, so your system is notified exactly when a run it started has finished.
Inbound webhook or API trigger?
Both let an outside system start a run. Pick by who is calling.
| API trigger | Inbound webhook | |
|---|---|---|
| Caller | Your own backend | A third party you do not control |
| Credential | A MagOneAI API key | A secret the sender signs with, or a static header value |
| Input shape | You send MagOneAI's input schema | The sender sends its own payload, you map it |
| Who adapts | Your code | MagOneAI, through field mappings |
If you cannot change what the sender sends, you want an inbound webhook.
One use case, several webhooks
A use case can carry several inbound webhooks, one per sender, up to 10 by default. A GitHub hook and a Stripe hook can feed the same workflow with completely different payload shapes.
Each webhook is fully independent. Each has its own:
- URL token
- Secret
- Field mappings
- Rate limit budget
- Delivery log
One sender's retry cannot suppress another's request, and one sender's secret does not verify against another's token.
The rate limit budget is per sender, not per use case. Three webhooks give one use case three times the verified-request budget. That is intended, because they genuinely are three different senders. There is deliberately no use-case-wide counter, because one noisy sender would then throttle a quiet one.
Names do not have to be unique. Two webhooks called "Stripe", one for production and one for sandbox, is a normal setup. The UI tells them apart by token prefix and creation date.
Requirements
A delivery is mapped and validated against the published schema, not the draft, because the published version is what actually runs. Publish before you point a sender at the URL. See Versions and publishing.
A webhook can start a form-interface use case only. A chat or hybrid use case is refused with a 400 rather than dispatched, because a webhook has no resumable chat session to attach the run to.
If a field mapping targets an audio or video input, the use case needs a resolvable transcription model: a transcription model configuration, or failing that the workflow default model.
Without one, the mapping is rejected when you save it. That is deliberate. The alternative would be a webhook that accepts every delivery and then fails every execution at the parse step.
Set up a webhook
Open the inbound webhooks section on the use case
In Studio, open the use case and find the Inbound webhooks section. If none exists yet, the panel prompts you to create the first one.
Choose an authentication mode
Set Auth mode. Two modes are available. See Authentication for how each one is verified.
- HMAC SHA-256: the sender signs the raw request body. Use this whenever the sender supports it.
- Shared secret: the sender sends a fixed value in a header. Use this only when the sender cannot sign.
For HMAC, set the signature header and prefix
Name the header the sender puts its signature in, and the prefix it puts in front of the hex digest, if any.
| Sender | Header | Prefix |
|---|---|---|
| GitHub | X-Hub-Signature-256 | sha256= |
| A custom sender | whatever it uses | often empty |
If the prefix does not match, verification fails, so copy it exactly.
Copy the secret, once
MagOneAI generates a secret and shows it in plaintext exactly once, on creation and again on each rotation. It is never shown again.
Copy it into the sender's configuration now. If you lose it, rotate the secret and update the sender.
Copy the receive URL
The URL is the webhook token on a fixed path:
POST https://<your-magoneai-host>/api/v1/inbound/{webhook_token}The token is a path identifier, not a credential, so the UI keeps it visible with a copy control. The secret is the credential.
Map the sender's payload to the Start node inputs
Add one row per field under Field mappings. See Field mapping.
Send a test delivery and read the delivery log
Trigger a real event on the sender's side, then open the delivery log on the webhook. Every request that got as far as signature verification is recorded there, successful or not, with the failure reason. See The delivery log.
Authentication
HMAC SHA-256
The sender computes an HMAC-SHA256 of the raw request body using the secret, hex-encodes it, and sends it in the header you configured.
X-Hub-Signature-256: sha256=<hex digest of the raw body>MagOneAI signs the same bytes it read off the wire and compares in constant time.
There is no timestamp and no time window. Third-party senders such as GitHub and Stripe sign once with their own scheme and do not send a MagOneAI timestamp. Replay protection is handled by delivery idempotency instead, not by a freshness window. See Idempotency.
This is a deliberate difference from outbound webhooks, where MagOneAI is the sender and does include a timestamp.
Shared secret
The sender puts a fixed secret value in a header, and MagOneAI compares it in constant time.
A shared secret is weaker than HMAC. The value is identical on every request, so anything that logs or proxies the request sees a reusable credential, and nothing binds it to the body.
Use HMAC whenever the sender supports it. Reach for a shared secret only for a sender that cannot sign.
What an unauthenticated request gets
An unknown token, an inactive webhook, and a webhook whose secret cannot be decrypted all return 404. They deliberately look identical, so probing cannot tell a real token with a broken secret from a token that does not exist.
Field mapping
A mapping row says: take this path out of the sender's JSON, and put it in this Start node input field.
Each row has three parts:
| Part | Meaning |
|---|---|
| Source | A dot-path into the sender's payload, for example data.object.customer_email |
| Target | The Start node input field to fill |
| Source type | How to interpret the value: value, url, or filename |
Source types
Valid against every input type except file, image and audio.
The value is coerced to the target's type, so a numeric string lands correctly in a number field. A url_image target also uses value, because a url_image field holds a URL string and nothing is fetched or stored for it.
Valid against a file, image or audio target, and against an array whose items are one of those types.
MagOneAI fetches the URL, checks it against SSRF rules and your allowed host list, validates the file's magic bytes, stores it, and extracts its text. Audio and video are transcribed. Only then does the execution start, with the resulting file IDs merged into its input.
For an array target, the sender sends a JSON array of URL strings:
{ "attachments": ["https://example.com/1.pdf", "https://example.com/2.pdf"] }Each URL is fetched and processed independently, and the results are merged back in the array's original order.
Optional, and only meaningful paired with a url row targeting the same field. Use it when the URL has no usable extension, such as a presigned S3 link.
A filename row with no matching url row is silently ignored. There is no filename pairing for array targets; each item's name is derived automatically.
Inline base64 is not supported. File, audio and video input is URL-fetch only.
Turning on file fetching
A webhook that maps any file field needs Accepts files turned on, and you should set Allowed fetch hosts to the hosts the sender's URLs actually come from. That host list is what stops a sender pointing MagOneAI at an arbitrary URL.
Idempotency
Senders retry. A webhook that starts a second execution on a retry would process the same event twice.
MagOneAI computes a delivery key per request and keeps it:
The Idempotency-Key header, if the sender sends one
This is the project's existing convention, and it is the most accurate option because the sender decides what counts as the same delivery.
Otherwise a hash of the request body
The body, not the signature header. In shared-secret mode the signature header is a static value, so hashing it would collapse every request from that sender into one bucket regardless of content.
A repeat delivery under a key already seen replays the original response: the same execution_id, no new execution, and no new delivery row.
Known limitation. Two genuinely different events with identical bodies and no Idempotency-Key header collapse into one delivery. If your sender can send a delivery id, configure it to send Idempotency-Key.
Responses
| Status | Meaning |
|---|---|
| 201 | A new execution was created. The response carries execution_id, poll_token and poll_url, and a Location header addressing the execution. |
| 202 | A duplicate. The original execution's details are replayed. |
| 400 | The payload could not be parsed, or the use case is chat or hybrid rather than form. |
| 404 | Unknown token, inactive webhook, or a secret that could not be read. |
| 413 | The body is larger than the cap. |
| 422 | The mapped input failed validation against the published schema. |
| 429 | A rate limit was hit, either per IP or per webhook. |
| 503 | The execution was created but the workflow engine could not be reached. The response carries Retry-After. |
On this route, 201 means new and 202 means replay. That is unusual, and it is the single most useful thing to know when debugging a sender's integration. If you see 202 where you expected work to happen, the sender is retrying a delivery MagOneAI already handled.
A sender that retries after a 503 is doing the right thing. MagOneAI deliberately clears the delivery key on a 503, so an identical retry is treated as new rather than being permanently stuck replaying a failed execution.
The delivery log
Every request that reaches signature verification writes a row, verified or not. Each row records whether the signature verified, the status code returned, the execution ID if one was created, the source IP, and the failure reason.
This log is the whole debugging story for a sender's integration. When a sender says "I posted and nothing happened", the log says whether the request arrived, whether it verified, and what MagOneAI did with it.
Rows are deleted after 30 days by default. Retention is not optional: an unauthenticated sender hammering a leaked token writes a row per attempt, so the table would otherwise grow without bound.
Limits
| Limit | Default | What it protects |
|---|---|---|
| Per-IP rate limit | 120 per minute | Applied before the token is even looked up, so a flood writes no delivery rows |
| Per-webhook budget | 60 per 60 seconds | Counted on verified requests only, so a leaked URL alone cannot exhaust a legitimate sender's budget |
| Max body size | 256 KB | Checked on the declared length and again while reading |
| Max characters in an unbounded text field | 10,000 | Stops a sender smuggling a very large string into any text field that has no explicit length limit of its own |
| Webhooks per use case | 10 | Bounds how many public surfaces one use case exposes |
| Delivery log retention | 30 days | Keeps the table bounded |
Files fetched in one delivery are additionally bounded by the platform's per-execution file limit, counted before any fetching starts so an oversized array is rejected early rather than after every file has been downloaded.
See Input limits.
Permissions
Two actions govern inbound webhooks, and they are separate from the outbound webhook actions:
| Action | Allows |
|---|---|
inbound_webhook.view | Reading a webhook's configuration and its delivery log |
inbound_webhook.manage | Creating, updating, deleting, and rotating the secret or token |
Permissions are scoped to the use case, so an inbound_webhook.manage grant covers every webhook on that use case. Per-webhook permissions are not expressible today.
The receive route itself is deliberately unauthenticated. The caller proves itself with its own signature, not with a MagOneAI session or key.
Rotation
Generates a new secret and shows it once. The sender must be updated, and deliveries signed with the old secret fail verification from that moment.
Rotate the secret when it may have leaked, or on a schedule your policy requires.
Generates a new receive URL. The old URL stops working immediately.
Rotate the token when the URL itself has leaked and you are seeing unauthenticated traffic in the delivery log.
Good patterns
Separate webhooks give you separate secrets, separate budgets and separate delivery logs. Sharing one webhook across two senders means one sender's noise affects the other, and a single secret rotation breaks both.
A sender's payload is usually far larger than your workflow needs. Mapping only what you use keeps the input small, the validation clear, and the workflow stable when the sender adds fields.
It is the difference between accurate deduplication and body-hash deduplication.
An empty host list on a file-accepting webhook means the sender chooses where MagOneAI fetches from.
Add an outbound webhook filtered to the inbound_webhook trigger type, and your system is told when the run it started has finished. See Outbound webhooks.
Deliveries validate against the published schema. A change you have not published is not what the webhook enforces.
Troubleshooting
Causes to check, in order: the URL has the wrong token, the webhook is inactive, or the webhook was deleted. All three return 404 on purpose so probing cannot tell them apart.
Causes to check:
- The signature header name does not match what the sender uses.
- The prefix is wrong. GitHub sends
sha256=in front of the digest; many custom senders send nothing. - The sender is signing something other than the raw body, such as a re-serialized version of it. MagOneAI signs the exact bytes it received.
- The secret was rotated and the sender still has the old one.
Cause: It is a duplicate. The delivery key matched one MagOneAI has already handled, so the original execution is replayed.
Fix: If the two events really are different, make the sender send distinct Idempotency-Key values. Identical bodies with no key collapse into one delivery.
Cause: The use case is chat or hybrid. A webhook has no chat session to resume, so only a form-interface use case can be started.
Fix: Use a form-interface use case, or wrap the chat workflow in one.
Causes to check: a source dot-path that does not exist in the payload, a required Start field with no mapping, or a value that cannot be coerced to the target's type.
The delivery log row carries the reason. Compare it against the published schema, not the draft you are editing.
Cause: The body exceeded 256 KB. A sender that embeds base64 content reaches this quickly.
Fix: Have the sender send a URL and map it with source_type: url, rather than embedding the file.
Cause: The per-IP limit fired, not the per-webhook one. The per-IP limit applies before the token is looked up, so unauthenticated traffic from the same IP consumes it.
Fix: Check the delivery log. Per-IP rejections write no row, so an empty log alongside 429s points at the per-IP limit.
Cause: The use case has neither a transcription model nor a default model, so there is nothing to transcribe with.
Fix: Set a transcription model, or a workflow default model, on the use case.
Cause: Executions are being created but the workflow engine is unreachable.
Fix: This is a platform issue, not a configuration one. The delivery rows are kept as your record. Retries of the same body are accepted afresh rather than being stuck as duplicates.