Configuring the real-time webhook

This page explains how to configure a real-time webhook that supports integration with DUO.

Before you begin

Ensure that you obtained the access token from your API client.

Procedure

Create the real-time webhook by using the following CURL example.

In the example, substitute the following values.
{{DUO API Hostname}}
The API hostname from your DUO tenant Auth API application configuration.
{{DUO Integration key}}
The Integration key from your DUO tenant Auth API application configuration.
{{tenant}}
Your Verify tenant name.
{{access token}}
The access token obtained from client credentials grant.

Webhook example CURL

curl --location --request POST 'https://{{tenant}}/v1.0/webhooks-mgmt/' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{access token}}}}' \
--data-raw '{
    "name": "DUO MFA Provider",
    "type": "realtime",
    "urls": ["{{DUO API Hostname}}"],
    "authentication": {
        "type": "header",
        "header": {
            "values":[
                {
                    "key": "hdr_integration_key",
                    "value": "{{DUO Integration key}}"
                },
                {
                    "key": "hdr_signing_key",
                    "value": "{{DUO Secret key}}"
                }
            ]
        }
    },
    "resources": {
        "enrollments": {
            "suffix": "/auth/v2/preauth",
            "method": "POST",
            "transform": {
                "outgoing": "{\"body\":jsonToFormURLEncoded({\"username\":body.username}, true), \"skip_authentication\": true, \"header\": header.put(\"date\", formatTime(now, '\''02 Jan 06 15:04 -0700'\'')).put(\"content-type\", \"application/x-www-form-urlencoded\").put(\"accept\", \"application/json\").put(\"authorization\", \"Basic \" + joinStrings([authentication_header.hdr_integration_key, hmacSha1(joinStrings([formatTime(now, '\''02 Jan 06 15:04 -0700'\''),method.toUpper(), host, path.toLower(), jsonToFormURLEncoded({\"username\":body.username}, true)], \"\\n\"), authentication_header.hdr_signing_key)], \":\").base64Encode())}",
                "incoming": "{\"body\": has(body.response) && has(body.response.devices) ? body.response.devices.map(d, d.capabilities.filter(c, c != \"auto\").map(c, {\"id\": d.device, \"capability\": c == \"mobile_otp\" ? \"hotp\" : c == \"sms\" ? \"smsotp\" : c, \"attributes\": {\"deviceName\": has(d.name) ? d.name : \"\", \"deliveryAddress\": has(d.number) ? d.number : \"\", \"authExecutionFlow\": c == \"mobile_otp\" ? \"validate\" : c == \"push\" ? \"init_then_poll\" : \"init_then_validate\"}})).flatten() : []}"
            }
        },
        "initiate": {
            "suffix": "/auth/v2/auth",
            "method": "POST",
            "transform": {
                "outgoing": "{\"body\":jsonToFormURLEncoded({\"async\":\"1\", \"device\":body.id, \"factor\": body.capability == \"smsotp\" ? \"sms\" : body.capability, \"username\":body.attributes.username}, true), \"skip_authentication\": true, \"header\": header.put(\"date\", formatTime(now, '\''02 Jan 06 15:04 -0700'\'')).put(\"content-type\", \"application/x-www-form-urlencoded\").put(\"accept\", \"application/json\").put(\"authorization\", \"Basic \" + joinStrings([authentication_header.hdr_integration_key, hmacSha1(joinStrings([formatTime(now, '\''02 Jan 06 15:04 -0700'\''),method.toUpper(), host, path.toLower(), jsonToFormURLEncoded({\"async\":\"1\", \"device\":body.id, \"factor\": body.capability == \"smsotp\" ? \"sms\" : body.capability, \"username\":body.attributes.username}, true)], \"\\n\"), authentication_header.hdr_signing_key)], \":\").base64Encode())}",
                "incoming": "{\"body\": has(body.response) && has(body.response.txid) ? {\"transactionId\": body.response.txid, \"status\": \"PENDING\"} : {\"status\": \"FAILED\"}}"
            }
        },
        "validate": {
            "suffix": "/auth/v2/auth",
            "method": "POST",
            "transform": {
                "outgoing": "{\"body\":jsonToFormURLEncoded({\"factor\": \"passcode\", \"passcode\": body.attributes.passvalue, \"username\":body.attributes.username}, true), \"skip_authentication\": true, \"header\": header.put(\"date\", formatTime(now, '\''02 Jan 06 15:04 -0700'\'')).put(\"content-type\", \"application/x-www-form-urlencoded\").put(\"accept\", \"application/json\").put(\"authorization\", \"Basic \" + joinStrings([authentication_header.hdr_integration_key, hmacSha1(joinStrings([formatTime(now, '\''02 Jan 06 15:04 -0700'\''),method.toUpper(), host, path.toLower(), jsonToFormURLEncoded({\"factor\": \"passcode\", \"passcode\": body.attributes.passvalue, \"username\":body.attributes.username}, true)], \"\\n\"), authentication_header.hdr_signing_key)], \":\").base64Encode())}",
                "incoming": "{\"body\": has(body.response) && has(body.response.result) ? {\"status\": body.response.result == \"allow\" ? \"SUCCESS\" : \"FAILED\", \"attributes\": body.response} : {\"status\": \"FAILED\"}}"
            }
        },
        "hotp_1": {
            "suffix": "/auth/v2/auth",
            "method": "POST",
            "transform": {
                "outgoing": "{\"body\":jsonToFormURLEncoded({\"factor\": \"passcode\", \"passcode\": body.attributes.passvalue, \"username\":body.attributes.username}, true), \"skip_authentication\": true, \"header\": header.put(\"date\", formatTime(now, '\''02 Jan 06 15:04 -0700'\'')).put(\"content-type\", \"application/x-www-form-urlencoded\").put(\"accept\", \"application/json\").put(\"authorization\", \"Basic \" + joinStrings([authentication_header.hdr_integration_key, hmacSha1(joinStrings([formatTime(now, '\''02 Jan 06 15:04 -0700'\''),method.toUpper(), host, path.toLower(), jsonToFormURLEncoded({\"factor\": \"passcode\", \"passcode\": body.attributes.passvalue, \"username\":body.attributes.username}, true)], \"\\n\"), authentication_header.hdr_signing_key)], \":\").base64Encode())}",
                "incoming": "{\"body\": has(body.response) && has(body.response.result) ? {\"status\": body.response.result == \"allow\" ? \"SUCCESS\" : \"FAILED\", \"attributes\": body.response} : {\"status\": \"FAILED\"}}"
            }
        },
        "result": {
            "suffix": "/auth/v2/auth_status",
            "method": "GET",
            "transform": {
                "outgoing": "{\"query\": {\"txid\": body.transactionId}, \"skip_authentication\": true, \"header\": header.put(\"date\", formatTime(now, '\''02 Jan 06 15:04 -0700'\'')).put(\"content-type\", \"application/x-www-form-urlencoded\").put(\"accept\", \"application/json\").put(\"authorization\", \"Basic \" + joinStrings([authentication_header.hdr_integration_key, hmacSha1(joinStrings([formatTime(now, '\''02 Jan 06 15:04 -0700'\''),method.toUpper(), host, path.toLower(), jsonToFormURLEncoded({\"txid\": body.transactionId}, true)], \"\\n\"), authentication_header.hdr_signing_key)], \":\").base64Encode())}",
                "incoming": "{\"body\": has(body.response) && has(body.response.result) ? {\"status\": body.response.result == \"allow\" ? \"SUCCESS\" : body.response.result == \"waiting\" || (body.response.result == \"deny\" && body.response.status == \"sent\") ?\"PENDING\" : body.response.status == \"timeout\" ? \"TIMEOUT\" : \"FAILED\", \"attributes\": body.response} : {\"status\": \"FAILED\"}}"
            }
        }
    },
    "purpose": ["external_mfa"]
}'
Take note of the id in the response body of the webhook that is created. It is needed to configure the DUO external MFA provider.