Error sending POST to webhook

I have a deployed MATTERMOST and I want to receive notifications about a successful backup, about an error during a backup there, a web hook is created, I add it to the parameter
–send-http-url=https://************************
–send-http-message=‘{“text”:“Test from Duplicati”}’
during the backup it gives an error:

HTTP Response request failed for: https://*********************** => Response status code does not indicate success: 400 (Bad Request).
Failed to send message: System.Net.Http.HttpRequestException: Response status code does not indicate success: 400 (Bad Request).
=> Response status code does not indicate success: 400 (Bad Request).

I thought the problem was in the format, I experimented, but I could not get the message to be sent.
I tried specifying the output format from duplicate to json. The link is correct, I removed it for security.
The message is sent correctly via powershell.

Just for clarity, do you mean if you run the backup from the powershell or running the POST directly with powershell.

Because you are surrounding with ‘ ‘ I assume the first, try to URL encode the JSON content. Keep the ` ` around it.

No, I don't do it in powershell, these are built-in functions in duplicate canary, but I can't use them because they have the wrong format, and it's not clear what's wrong with them.

I think the dev was asking what that means. Basically how does powershell get used?

If you mean you found a way to send directly from powershell, can you give example?
Redact sensitive information, but right now seeing a 400 error is not enough guidance.
There are many different ways to send HTTP, and receivers have their own limitations.

Next question would therefore be what’s the webhook sent to? That controls rejections.
Possibly the provider or software can be researched. Without that it gets into guessing,
unless of course your webhook receiver is unencrypted, in which case looking can help.

Http notifications and ntfy.sh is an example of a webhook receiver limitation.
Yours might be different, but there’s no information on that or on powershell.

Another way to compare against something that works is to use a tester site.
webhook.site is an example, but there are other sites that can show result.

I spun up a quick server to output what was sent by Duplicati with those parameters:

Headers: {
host: ‘localhost:3000’,
‘content-type’: ‘application/x-www-form-urlencoded’,
‘content-length’: ‘98’
}
Content-Type: application/x-www-form-urlencoded

Body: { message: ‘‘{“text”:“Test from Duplicati”}’’ }

I don’t know Mattermost but perhaps the content-type expected for a body with Json might be causing the 400.

What method of the api from Mattermost are you using?

Does “during a backup there” mean a backup of Mattermost by Duplicati, reporting
elsewhere, a backup of external Duplicati system reporting to Mattermost, or what?

Incoming webhooks documentation sounds very particular about a lot of the format.
Using PowerShell or curl (or Duplicati) would probably want customization to satisfy.
Duplicati is less configurable than curl or Invoke-WebRequest. Is that your test use?

Some things are probably optional, so if you have a minimal example, please post it.

This is still guessing too much about your webhook usage, so please also clarify that.

If you’re trying to receive webhook into Mattermost from something, it’s unclear if the:

Content-Type: application/json can be replaced with another Content-Type, but
reference to Slack webhooks could probably be researched, but what’s the use case?

EDIT 1:

I tried looking at Slack docs, and about all I could tell is they want application/json.

Maybe you need this one:

  --send-http-json-urls (String): HTTP report URLs for sending JSON data
    Use this option to set HTTP report URLs for sending JSON data. This option
    accepts multiple URLs, seperated by a semi-colon. All URLs will receive the
    same data. Note that this option ignores the format and verb settings.

Lots of questions and suggestions above, but when original post says:

do you mean by

  --send-http-result-output-format (Enumeration): Select the output format for results
    Use this option to select the output format for results. Available formats:
    Duplicati, Json
    * values: Duplicati, Json
    * default value: Duplicati

I think they are predefined message formats, certainly not just as Mattermost might like.
This gets back to the question of what you want. A webhook per their format directions?

You can certainly create Scripts to do what you like, and send what you like, as you like.

My task is simple, there is a built-in function waiting for POST with HEADER application/json, with the body content "text".

When sending with standard tools, I get 400 - which means the format received for the Mattermost API is incorrect. The API is standard and webhooks are used in the CE version of the service, without any additional add-ons and works completely natively.

def send_to_mattermost(message):
payload = {
“username”: “Telegram”,
“text”: message,
“icon_url”: “”
}
try:
requests.post(MATTERMOST, json=payload, timeout=5)
except Exception as e:
print(f"Ошибка отправки в Mattermost: {e}")

Also, the functions of the advanced parameters, as for me, are extremely inconvenient… but I will clearly not complain about this.

From examples of a fully working script, I made my project with notifications on Mattermost with the following

Yes, there are other values ​​that are responsible for something slightly different, but their absence did not prevent me from sending POST

I tried, but standard messages are not sent with the same error 400, when specifying the format and including with a custom message, as if the body calls the parameter “text” differently.

Probably the problem lies in the header itself because it clearly expects not application/x-www-form-urlencoded but application/json and this can cause problems, but how to change it, maybe there is some parameter? If there is, then I did not find it.

I’m still not hearing anything about PowerShell. Was that once used? Did it work?

Meanwhile, it looks like you try to use Python with requests, and it’s failing. See
More complicated POST requests whose example looks a lot to me like your plan.
It reads like it should be setting right header and content, but I don’t use requests.

The hope was to see some send that works, to try to get Duplicati to do the same.
If nothing at all sends to your receiver (is that in Mattermost), hope of that is small.

Built into Mattermost? Got a link saying how that works?

If it never works with anything, that’s out of scope here.

Here’s send-http-json-urls into a simple receiver. Looks OK if you like its content.

$ nc -l -p 8000
POST / HTTP/1.1
Host: 192.168.86.178:8000
Content-Type: application/json
Content-Length: 3046

{"Data":{"DeletedFiles":0,"DeletedFolders":0,"ModifiedFiles":0,"ExaminedFiles":1,"OpenedFiles":0,"AddedFiles":0,"SizeOfModifiedFiles":0,"SizeOfAddedFiles":0,"SizeOfExaminedFiles":39,"SizeOfOpenedFiles":0,"NotProcessedFiles":0,"AddedFolders":0,"TooLargeFiles":0,"FilesWithError":0,"ModifiedFolders":0,"ModifiedSymlinks":0,"AddedSymlinks":0,"DeletedSymlinks":0,"PartialBackup":false,"Dryrun":false,"MainOperation":"Backup","CompactResults":null,"VacuumResults":null,"DeleteResults":null,"RepairResults":null,"TestResults":{"MainOperation":"Test","VerificationsActualLength":3,"Verifications":[{"Key":"duplicati-20250601T194642Z.dlist.zip.aes","Value":[]},{"Key":"duplicati-i3b430982f6bf4fbfb75a9add96646723.dindex.zip.aes","Value":[]},{"Key":"duplicati-b025305832d4940ff81ba5a3e732914f8.dblock.zip.aes","Value":[]}],"ParsedResult":"Success","Interrupted":false,"Version":"2.1.0.5 (2.1.0.5_stable_2025-03-04)","EndTime":"2025-08-22T21:10:48.8864264Z","BeginTime":"2025-08-22T21:10:47.7364359Z","Duration":"00:00:01.1499905","MessagesActualLength":0,"WarningsActualLength":0,"ErrorsActualLength":0,"BackendStatistics":{"RemoteCalls":5,"BytesUploaded":0,"BytesDownloaded":3079,"FilesUploaded":0,"FilesDownloaded":3,"FilesDeleted":0,"FoldersCreated":0,"RetryAttempts":0,"UnknownFileSize":0,"UnknownFileCount":0,"KnownFileCount":3,"KnownFileSize":3079,"LastBackupDate":"2025-06-01T15:46:42-04:00","BackupListCount":1,"TotalQuotaSpace":999618043904,"FreeQuotaSpace":109748785152,"AssignedQuotaSpace":-1,"ReportedQuotaError":false,"ReportedQuotaWarning":false,"MainOperation":"Backup","ParsedResult":"Success","Interrupted":false,"Version":"2.1.0.5 (2.1.0.5_stable_2025-03-04)","EndTime":"0001-01-01T00:00:00","BeginTime":"2025-08-22T21:10:46.3689627Z","Duration":"00:00:00","MessagesActualLength":0,"WarningsActualLength":0,"ErrorsActualLength":0}},"ParsedResult":"Success","Interrupted":false,"Version":"2.1.0.5 (2.1.0.5_stable_2025-03-04)","EndTime":"2025-08-22T21:10:49.7435569Z","BeginTime":"2025-08-22T21:10:46.3689531Z","Duration":"00:00:03.3746038","MessagesActualLength":12,"WarningsActualLength":0,"ErrorsActualLength":0,"BackendStatistics":{"RemoteCalls":5,"BytesUploaded":0,"BytesDownloaded":3079,"FilesUploaded":0,"FilesDownloaded":3,"FilesDeleted":0,"FoldersCreated":0,"RetryAttempts":0,"UnknownFileSize":0,"UnknownFileCount":0,"KnownFileCount":3,"KnownFileSize":3079,"LastBackupDate":"2025-06-01T15:46:42-04:00","BackupListCount":1,"TotalQuotaSpace":999618043904,"FreeQuotaSpace":109748785152,"AssignedQuotaSpace":-1,"ReportedQuotaError":false,"ReportedQuotaWarning":false,"MainOperation":"Backup","ParsedResult":"Success","Interrupted":false,"Version":"2.1.0.5 (2.1.0.5_stable_2025-03-04)","EndTime":"0001-01-01T00:00:00","BeginTime":"2025-08-22T21:10:46.3689627Z","Duration":"00:00:00","MessagesActualLength":0,"WarningsActualLength":0,"ErrorsActualLength":0}},"Extra":{"OperationName":"Backup","machine-id":"5719973b9fa74b9d93d70300e8a5fd58","machine-name":"HP4","backup-name":"test 4","backup-id":"DB-7"},"LogLines":[],"Exception":null}

Test adding send-http-message with default value Duplicati %OPERATIONNAME% report for %backup-name% (%machine-id%, %backup-id%, %machine-name%)%RESULT% looks similar. I wonder if it doesn’t work with send-http-json-urls? Not my area of expertise.

You don’t say what options you use, but I’d guess the above is the “custom message” (but maybe not respected in my test). I previously guessed on “specifying the format”, so next I

Test adding send-http-result-output-format=Json, and it still sends the same format.

Test changing send-http-json-urls to send-http-url. Still sends the same old format.

Test changing to send-http-result-output-format=Duplicati, and its format changes:

POST / HTTP/1.1
Host: 192.168.86.178:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: 3398

message=Duplicati%20Backup%20report%20for%20test%204%20%285719973b9fa74b9d93d70300e8a5fd58%2C%20DB-7%2C%20HP4%29%0D%0A%0D%0ADeletedFiles%3A%200%0D%0ADeletedFolders%3A%200%0D%0AModifiedFiles%3A%200%0D%0AExaminedFiles%3A%201%0D%0AOpenedFiles%3A%200%0D%0AAddedFiles%3A%200%0D%0ASizeOfModifiedFiles%3A%200%0D%0ASizeOfAddedFiles%3A%200%0D%0ASizeOfExaminedFiles%3A%2039%0D%0ASizeOfOpenedFiles%3A%200%0D%0ANotProcessedFiles%3A%200%0D%0AAddedFolders%3A%200%0D%0ATooLargeFiles%3A%200%0D%0AFilesWithError%3A%200%0D%0AModifiedFolders%3A%200%0D%0AModifiedSymlinks%3A%200%0D%0AAddedSymlinks%3A%200%0D%0ADeletedSymlinks%3A%200%0D%0APartialBackup%3A%20False%0D%0ADryrun%3A%20False%0D%0AMainOperation%3A%20Backup%0D%0AParsedResult%3A%20Success%0D%0AInterrupted%3A%20False%0D%0AFatal%3A%20False%0D%0AVersion%3A%202.1.0.5%20%282.1.0.5_stable_2025-03-04%29%0D%0AEndTime%3A%208%2F22%2F2025%205%3A40%3A54%20PM%20%281755898854%29%0D%0ABeginTime%3A%208%2F22%2F2025%205%3A40%3A53%20PM%20%281755898853%29%0D%0ADuration%3A%2000%3A00%3A01.2718811%0D%0AMessagesActualLength%3A%2012%0D%0AWarningsActualLength%3A%200%0D%0AErrorsActualLength%3A%200%0D%0ALimitedMessages%3A%20%5B%0D%0A%20%20%20%202025-08-22%2017%3A40%3A53%20-04%20-%20%5BInformation-Duplicati.Library.Main.Controller-StartingOperation%5D%3A%20The%20operation%20Backup%20has%20started%2C%0D%0A%20%20%20%202025-08-22%2017%3A40%3A53%20-04%20-%20%5BInformation-Duplicati.Library.Main.BasicResults-BackendEvent%5D%3A%20Backend%20event%3A%20List%20-%20Started%3A%20%20%28%29%2C%0D%0A%20%20%20%202025-08-22%2017%3A40%3A53%20-04%20-%20%5BInformation-Duplicati.Library.Main.BasicResults-BackendEvent%5D%3A%20Backend%20event%3A%20List%20-%20Completed%3A%20%20%283%20bytes%29%2C%0D%0A%20%20%20%202025-08-22%2017%3A40%3A54%20-04%20-%20%5BInformation-Duplicati.Library.Main.BasicResults-BackendEvent%5D%3A%20Backend%20event%3A%20List%20-%20Started%3A%20%20%28%29%2C%0D%0A%20%20%20%202025-08-22%2017%3A40%3A54%20-04%20-%20%5BInformation-Duplicati.Library.Main.BasicResults-BackendEvent%5D%3A%20Backend%20event%3A%20List%20-%20Completed%3A%20%20%283%20bytes%29%2C%0D%0A%20%20%20%202025-08-22%2017%3A40%3A54%20-04%20-%20%5BInformation-Duplicati.Library.Main.BasicResults-BackendEvent%5D%3A%20Backend%20event%3A%20Get%20-%20Started%3A%20duplicati-20250601T194642Z.dlist.zip.aes%20%281021%20bytes%29%2C%0D%0A%20%20%20%202025-08-22%2017%3A40%3A54%20-04%20-%20%5BInformation-Duplicati.Library.Main.BasicResults-BackendEvent%5D%3A%20Backend%20event%3A%20Get%20-%20Completed%3A%20duplicati-20250601T194642Z.dlist.zip.aes%20%281021%20bytes%29%2C%0D%0A%20%20%20%202025-08-22%2017%3A40%3A54%20-04%20-%20%5BInformation-Duplicati.Library.Main.BasicResults-BackendEvent%5D%3A%20Backend%20event%3A%20Get%20-%20Started%3A%20duplicati-i3b430982f6bf4fbfb75a9add96646723.dindex.zip.aes%20%28989%20bytes%29%2C%0D%0A%20%20%20%202025-08-22%2017%3A40%3A54%20-04%20-%20%5BInformation-Duplicati.Library.Main.BasicResults-BackendEvent%5D%3A%20Backend%20event%3A%20Get%20-%20Completed%3A%20duplicati-i3b430982f6bf4fbfb75a9add96646723.dindex.zip.aes%20%28989%20bytes%29%2C%0D%0A%20%20%20%202025-08-22%2017%3A40%3A54%20-04%20-%20%5BInformation-Duplicati.Library.Main.BasicResults-BackendEvent%5D%3A%20Backend%20event%3A%20Get%20-%20Started%3A%20duplicati-b025305832d4940ff81ba5a3e732914f8.dblock.zip.aes%20%281.04%20KB%29%2C%0D%0A...%0D%0A%5D%0D%0ALimitedWarnings%3A%20%5B%5D%0D%0ALimitedErrors%3A%20%5B%5D

So now change to --send-http-message=test for something more custom and compact.

POST / HTTP/1.1
Host: 192.168.86.178:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: 12

message=test

Change send-http-url back to send-http-json-urls and it’s back to application/json, however it’s also back to the standard JSON stats that were typical early on in these tests.

I don’t know what you did or even if you did it in Duplicati. What’s body and parameter talk?

Regardless, there’s some Duplicati testing for you on 2.1.0.5, and I didn’t test a newer one.

For your Python script, I added indentation back and tried it to webhook.site which reports:

content-type application/json and content {"username": "Telegram", "text": "test", "icon_url": ""}

From Duplicati view, some option interactions (or lack thereof) could be documented more.

EDIT 1:

Added send-http-extra-parameters=parameter1=value1 but had to send-http-url.

POST / HTTP/1.1
Host: 192.168.86.178:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: 32

message=test&parameter1%3Dvalue1

The combination of application/json plus customized data send still appears elusive, meaning you might have to send post with an external tool – except those are failing too.

Can’t help much more if external sender can’t communicate with your webhook receiver.

EDIT 2:

These new options allows multiple urls to be set, and will ignore the verb and format parameters, but otherwise work the same.

is the change and says some of what is ignored, but how are the users informed of that?

I wonder if some other options have long been ignored so aren’t specifically named now.
Having seen send-http-url respect send-http-extra-parameters and send-http-message just above, but with unwanted Content-Type and not JSON, change options to send-http-result-output-format=Json and both those data options are now ignored.

There isn’t at present a parameter to force Content-Type to application/json as confirmed by @ts678 on his tests bellow.

Not sure I agree with that (as seen from the outputs I posted), but getting JSON exactly as one might desire looks hard. Duplicati is not a general-purpose tool for arbitrary webhooks.

JSON serializer ignores ‘send-http-extra-parameters’ & ‘send-http-message’ in HTTP message #3448 (half?) fix might be why I can use send-http-extra-parameters in new version.

  --send-http-extra-parameters (String): Extra parameters for the message sent
    Use this option to set extra parameters for the message body. This
    parameter can either be a querystring (e.g.
    'parameter1=value1&parameter2=value2') or a JSON key/value object.

So far I have 2.1.1.100_canary_2025-08-08 sending JSON which pretty-prints like this:

{
  "Data": {
    "DeletedFiles": 0,
...
      "ErrorsActualLength": 0
    }
  },
  "Extra": {
    "backup-name": "test 1",
    "backup-id": "DB-2",
    "machine-id": "fa80d81bcc2c4ff08c7ada0bfc2ac81c",
    "machine-name": "HP4",
    "operating-system": "Windows",
    "installation-type": "win-x64-gui.zip",
    "destination-type": "file",
    "next-scheduled-run": null,
    "update-channel": "Canary",
    "parameter1": "value1",
    "parameter2": "value2"
  },
  "LogLines": [],
  "Exception": null
}

and this is with

--send-http-message=C:\tmp\json.txt
--send-http-result-output-format=Json
--send-http-url=http://192.168.86.178:8000/
--send-http-extra-parameters=parameter1=value1&parameter2=value2

I still haven’t been able to get it to --send-http-message, but can set Content-Type.

POST / HTTP/1.1
Host: 192.168.86.178:8000
traceparent: 00-1f33e532cfe26609c0c07951e28fcf99-ced50a644d125e64-00
Content-Type: application/json
Content-Length: 3227

As for sending custom data, I used the example from help text and the data did get sent, however it might not be exactly in JSON position that a webhook receiver might prefer it.

EDIT 1:

The issue that was closed says:

The --send-http-extra-parameters were added as a hacky way to add custom data.
With #6078 this is changed to be data input. For JSON reports, this will add any values into the Extra element in the JSON object.

For template based (i.e. text) the keys need to be in the template or they will be ignored.

and I’m not sure if the last line says that JSON templates don’t work. They don’t seem to.

Here’s what I was trying to force. I see it reading from file, but the contents don’t get sent:

{“username”: “Telegram”, “text”: “test”, “icon_url”: “”}

EDIT 2:

The last line here also dims my hope that a JSON template works (but why shouldn’t it?):

EDIT 3:

One reason that JSON template could get complicated is that replacements might turn what used to be good JSON into bad JSON, so one would have to plan some handling.

EDIT 4:

Additionally, the template mechanism has inherent limitations. A script that looks at the results of the operation, decides what to say, and sends it will have a lot more flexibility.

EDIT 5:

I’d think that an alternative to getting the webhook format perfect on first send is to have something receive it, look it over (fields are already well broken out), and send webhook.

On character escapes question, there seem to be lots of libraries available that can do it. Something’s fortunately already doing it for the extra values that I configured in ngax UI (because ngclient seems to trip over special characters). For example, if I set the option:

--send-http-extra-parameters=parameter1=escape"\test

webhook receiver sees what looks OK to this non-JSON expert, with characters escaped:

"parameter1":"escape\"\\test"

The issue is I believe he needs to specify a totally custom body so as to be accepted by the API on mattermost and enforce the application/json which there isn’t really a way to do.

I am not even sure how he would deal with authentication needs of his endpoint.

IMO the only route is for him to run a script where he can tailor the request and handle authentication process.

We have no information beyond that “there is a built-in function” waiting – somewhere.

It does look to me like Mattermost “Incoming webhooks” demands very specific JSON.

Mattermost authentication is described as being by secret xxx-generatedkey-xxx.

It might be easiest, despite the report that standard tools into (what?) get a 400 error.

Google webhook transform and resend for the other way that I’m thinking might work.

EDIT 1:

I’m looking at the AI Overview which is sometimes optimistic, but for another answer:

Google python webhook library receive

and it brings up def receive_webhook(): code in Python that uses the Flask library.

Since it appears a Python sender has been made, add a Python receiver in front of it?

Use a Duplicati new enough to let you send Extra data. Extract what you like. Resend.

I still suspect that a run-script-after is easier, but I think there are other options as well.

It seems to use a Bearer token, in any event, there is no send-http* option to add a header.

We could add this as a feature request, to have extra headers and content type set, what do you think?

send-http-content-type
send-http-extra-header (repeatable? or a json with the array of extra headers)

Google webhook transform and resend for the other way that I’m thinking might work.

True, but if he wanted that endpoint authenticated we are back to the header issue.

It depends on what is being used. Incoming webhooks documentation gives example:

POST /hooks/xxx-generatedkey-xxx HTTP/1.1
Host: your-mattermost-server.com
Content-Type: application/json
Content-Length: 630

{
  "channel": "town-square",
  "username": "test-automation",
  "icon_url": "https://mattermost.com/wp-content/uploads/2022/02/icon.png",
  "text": "#### Test results for July 27th, 2017\n@channel please review failed tests.\n\n| Component  | Tests Run   | Tests Failed                                   |\n|:-----------|:-----------:|:-----------------------------------------------|\n| Server     | 948         | ✅ 0                           |\n| Web Client | 123         | ⚠️ 2 [(see details)](https://linktologs) |\n| iOS Client | 78          | ⚠️ 3 [(see details)](https://linktologs) |"
}

and for security, it cautions:

You will end up with a webhook endpoint that looks like so:

https://your-mattermost-server.com/hooks/xxx-generatedkey-xxx

Treat this endpoint as a secret. Anyone who has it will be able to post messages to your Mattermost instance.

Not if the standard incoming webhook feature is used. The auth is by data in the URL.

I would note that the Python script is using username, text, and icon_url from this doc.

Maybe the intent is to hit the Mattermost incoming webhook facility, but we don’t know.

EDIT 1:

I posted an edit to prior post, suggesting a Python receiver and resender if it’s needed.

I think that the Content-Type is handled, JSON template isn’t, and headers aren’t either.

Authenticating the webhook notification in Wikipedia has info. This one is shared secret.

Changing Content-Type might be by this:

I was looking at this authentication & using the method Post.

The issue is when you opt for ResultExportFormat.Json, then there is no such thing as a template that is formatted as with using Duplicati’s format.

Perhaps it could be expanded into ResultExportFormat.CustomJson, where the variables are avaliable such as they are when using ResultExportFormat.Duplicati, including an extra one like %REPORTJSON% so it could be added as a child to the custom json.