Installations
Exchange an install token for installation credentials
Redeem the one-time exchange token that Fluid sends in a droplet’s install webhook for the installation’s permanent credentials: its Bearer authentication_token and its webhook_verification_token.
This endpoint takes no Authorization header — the exchange token is the credential. Call it from your droplet’s backend as soon as the install webhook arrives.
- An exchange token redeems once, and expires 10 minutes after it is issued.
- A token that has already been redeemed, or has expired, returns 410. Fluid does not issue a replacement through this endpoint.
- An unknown token returns 404. So does a token whose installation is no longer active, or whose droplet can no longer be installed (for example, an archived droplet). Those cases do not use up the token.
POST
/
api
/
droplet_installations
/
exchange
Exchange an install token for installation credentials
curl --request POST \
--url https://api.fluid.app/api/droplet_installations/exchange \
--header 'Content-Type: application/json' \
--data '
{
"exchange_token": "dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE"
}
'import requests
url = "https://api.fluid.app/api/droplet_installations/exchange"
payload = { "exchange_token": "dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE" }
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({exchange_token: 'dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE'})
};
fetch('https://api.fluid.app/api/droplet_installations/exchange', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.fluid.app/api/droplet_installations/exchange",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'exchange_token' => 'dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.fluid.app/api/droplet_installations/exchange"
payload := strings.NewReader("{\n \"exchange_token\": \"dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.fluid.app/api/droplet_installations/exchange")
.header("Content-Type", "application/json")
.body("{\n \"exchange_token\": \"dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fluid.app/api/droplet_installations/exchange")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"exchange_token\": \"dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE\"\n}"
response = http.request(request)
puts response.read_body{
"droplet_installation": {
"droplet_installation_uuid": "9c3e7a41-5b2d-4f8e-a0c6-1d7b4e92f358",
"droplet_uuid": "4f2a9c1e-7b3d-4e8a-9f61-2c5d8b0e3a17",
"fluid_company_id": 1042,
"fluid_shop": "northwind.fluid.app"
},
"credentials": {
"authentication_token": "dit_Hk4mZp8Qx2Wv6Tn9Rb3Lc7Yf1Sd5Ga0J",
"webhook_verification_token": "wvt_Nq2Xe7Kr5Ub9Pm3Vh8Jt1Ws6Cz4Fy0D",
"issued_at": "2026-09-30T14:05:12Z",
"token_type": "bearer"
},
"meta": {
"request_id": "3b1f6c2e-8d4a-4e7b-9f05-a2c1d6e8b347",
"timestamp": "2026-09-30T14:05:12Z"
}
}{
"error_message": "not found",
"errors": {
"exchange_token": [
"invalid or not found"
]
},
"meta": {
"request_id": "3b1f6c2e-8d4a-4e7b-9f05-a2c1d6e8b347",
"timestamp": "2026-09-30T14:05:12Z"
}
}{
"error_message": "gone",
"errors": {
"exchange_token": [
"expired or already used"
]
},
"meta": {
"request_id": "3b1f6c2e-8d4a-4e7b-9f05-a2c1d6e8b347",
"timestamp": "2026-09-30T14:05:12Z"
}
}{
"error_message": "Invalid parameters",
"errors": {
"exchange_token": [
"is missing"
]
},
"meta": {
"request_id": "3b1f6c2e-8d4a-4e7b-9f05-a2c1d6e8b347",
"timestamp": "2026-09-30T14:05:12Z"
}
}Body
application/json
The one-time exchange token delivered in the droplet's install webhook. It starts with dex_.
Response
The token was redeemed. Store both credentials; this is the only time they are returned.
⌘I
Exchange an install token for installation credentials
curl --request POST \
--url https://api.fluid.app/api/droplet_installations/exchange \
--header 'Content-Type: application/json' \
--data '
{
"exchange_token": "dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE"
}
'import requests
url = "https://api.fluid.app/api/droplet_installations/exchange"
payload = { "exchange_token": "dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE" }
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({exchange_token: 'dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE'})
};
fetch('https://api.fluid.app/api/droplet_installations/exchange', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.fluid.app/api/droplet_installations/exchange",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'exchange_token' => 'dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.fluid.app/api/droplet_installations/exchange"
payload := strings.NewReader("{\n \"exchange_token\": \"dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.fluid.app/api/droplet_installations/exchange")
.header("Content-Type", "application/json")
.body("{\n \"exchange_token\": \"dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.fluid.app/api/droplet_installations/exchange")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"exchange_token\": \"dex_q7Xv2mR9kLp4TzW8nB3cYh6JfD1sAe5GuN0oKiVbQwE\"\n}"
response = http.request(request)
puts response.read_body{
"droplet_installation": {
"droplet_installation_uuid": "9c3e7a41-5b2d-4f8e-a0c6-1d7b4e92f358",
"droplet_uuid": "4f2a9c1e-7b3d-4e8a-9f61-2c5d8b0e3a17",
"fluid_company_id": 1042,
"fluid_shop": "northwind.fluid.app"
},
"credentials": {
"authentication_token": "dit_Hk4mZp8Qx2Wv6Tn9Rb3Lc7Yf1Sd5Ga0J",
"webhook_verification_token": "wvt_Nq2Xe7Kr5Ub9Pm3Vh8Jt1Ws6Cz4Fy0D",
"issued_at": "2026-09-30T14:05:12Z",
"token_type": "bearer"
},
"meta": {
"request_id": "3b1f6c2e-8d4a-4e7b-9f05-a2c1d6e8b347",
"timestamp": "2026-09-30T14:05:12Z"
}
}{
"error_message": "not found",
"errors": {
"exchange_token": [
"invalid or not found"
]
},
"meta": {
"request_id": "3b1f6c2e-8d4a-4e7b-9f05-a2c1d6e8b347",
"timestamp": "2026-09-30T14:05:12Z"
}
}{
"error_message": "gone",
"errors": {
"exchange_token": [
"expired or already used"
]
},
"meta": {
"request_id": "3b1f6c2e-8d4a-4e7b-9f05-a2c1d6e8b347",
"timestamp": "2026-09-30T14:05:12Z"
}
}{
"error_message": "Invalid parameters",
"errors": {
"exchange_token": [
"is missing"
]
},
"meta": {
"request_id": "3b1f6c2e-8d4a-4e7b-9f05-a2c1d6e8b347",
"timestamp": "2026-09-30T14:05:12Z"
}
}