curl --request POST \
--url https://app.sideshift.app/api/oauth/v1/ads/google/forecast \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"connectionId": "<string>",
"customerId": "<string>",
"currencyCode": "<string>",
"startDate": "<string>",
"budget": {
"amountMicros": "<string>"
},
"bidding": {
"strategy": "maximize_clicks"
},
"locationIds": [
"<string>"
],
"languageIds": [
"<string>"
],
"placements": [],
"endDate": "<string>",
"goals": {
"mode": "account"
},
"audience": {
"mode": "all"
}
}
'import requests
url = "https://app.sideshift.app/api/oauth/v1/ads/google/forecast"
payload = {
"connectionId": "<string>",
"customerId": "<string>",
"currencyCode": "<string>",
"startDate": "<string>",
"budget": { "amountMicros": "<string>" },
"bidding": { "strategy": "maximize_clicks" },
"locationIds": ["<string>"],
"languageIds": ["<string>"],
"placements": [],
"endDate": "<string>",
"goals": { "mode": "account" },
"audience": { "mode": "all" }
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
connectionId: '<string>',
customerId: '<string>',
currencyCode: '<string>',
startDate: '<string>',
budget: {amountMicros: '<string>'},
bidding: {strategy: 'maximize_clicks'},
locationIds: ['<string>'],
languageIds: ['<string>'],
placements: [],
endDate: '<string>',
goals: {mode: 'account'},
audience: {mode: 'all'}
})
};
fetch('https://app.sideshift.app/api/oauth/v1/ads/google/forecast', 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://app.sideshift.app/api/oauth/v1/ads/google/forecast",
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([
'connectionId' => '<string>',
'customerId' => '<string>',
'currencyCode' => '<string>',
'startDate' => '<string>',
'budget' => [
'amountMicros' => '<string>'
],
'bidding' => [
'strategy' => 'maximize_clicks'
],
'locationIds' => [
'<string>'
],
'languageIds' => [
'<string>'
],
'placements' => [
],
'endDate' => '<string>',
'goals' => [
'mode' => 'account'
],
'audience' => [
'mode' => 'all'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"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://app.sideshift.app/api/oauth/v1/ads/google/forecast"
payload := strings.NewReader("{\n \"connectionId\": \"<string>\",\n \"customerId\": \"<string>\",\n \"currencyCode\": \"<string>\",\n \"startDate\": \"<string>\",\n \"budget\": {\n \"amountMicros\": \"<string>\"\n },\n \"bidding\": {\n \"strategy\": \"maximize_clicks\"\n },\n \"locationIds\": [\n \"<string>\"\n ],\n \"languageIds\": [\n \"<string>\"\n ],\n \"placements\": [],\n \"endDate\": \"<string>\",\n \"goals\": {\n \"mode\": \"account\"\n },\n \"audience\": {\n \"mode\": \"all\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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://app.sideshift.app/api/oauth/v1/ads/google/forecast")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"connectionId\": \"<string>\",\n \"customerId\": \"<string>\",\n \"currencyCode\": \"<string>\",\n \"startDate\": \"<string>\",\n \"budget\": {\n \"amountMicros\": \"<string>\"\n },\n \"bidding\": {\n \"strategy\": \"maximize_clicks\"\n },\n \"locationIds\": [\n \"<string>\"\n ],\n \"languageIds\": [\n \"<string>\"\n ],\n \"placements\": [],\n \"endDate\": \"<string>\",\n \"goals\": {\n \"mode\": \"account\"\n },\n \"audience\": {\n \"mode\": \"all\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.sideshift.app/api/oauth/v1/ads/google/forecast")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"connectionId\": \"<string>\",\n \"customerId\": \"<string>\",\n \"currencyCode\": \"<string>\",\n \"startDate\": \"<string>\",\n \"budget\": {\n \"amountMicros\": \"<string>\"\n },\n \"bidding\": {\n \"strategy\": \"maximize_clicks\"\n },\n \"locationIds\": [\n \"<string>\"\n ],\n \"languageIds\": [\n \"<string>\"\n ],\n \"placements\": [],\n \"endDate\": \"<string>\",\n \"goals\": {\n \"mode\": \"account\"\n },\n \"audience\": {\n \"mode\": \"all\"\n }\n}"
response = http.request(request)
puts response.read_body{
"data": {
"provider": "google",
"source": "google_reach_plan",
"status": "ready",
"currencyCode": "<string>",
"window": {
"startDate": "<string>",
"endDate": "<string>",
"planningHorizon": true
},
"scope": {
"requestedLanguageIds": [
"<string>"
],
"modeledLanguages": "all",
"metricBasis": "on_target",
"limitation": "All languages — Google forecasts do not model language targeting."
},
"reach": 4503599627370495,
"impressions": 4503599627370495,
"clicks": 4503599627370495,
"forecastSpendMicros": "<string>",
"reason": "access_required",
"message": "<string>",
"conversionRateSource": "google_suggestion",
"conversionRate": 123,
"conversionRateModel": "CUSTOMER_HISTORY",
"productCode": "<string>"
}
}{
"error": {
"code": "unauthorized",
"message": "Missing bearer access token",
"requestId": "req_..."
}
}{
"error": {
"code": "insufficient_scope",
"message": "Requires scope 'campaigns:write'",
"requestId": "req_..."
}
}{
"error": {
"code": "not_found",
"message": "Resource not found",
"requestId": "req_..."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded",
"requestId": "req_..."
}
}Read a native Google Demand Gen reach forecast
Read-only ReachPlan request for the selected, verified Google Ads account. Google must permit ReachPlan access and return a compatible Demand Gen planning product and a valid historical conversion-rate suggestion. Expected customerId must match the current connection; a same-currency account rebind returns 409. Only customer-history conversion-rate suggestions are used and the actual rate/model is returned. Exact contiguous demographic ranges are supported when listed by the product metadata; saved audience constraints and disjoint or partial unknown-age ranges return unavailable. The forecast models all languages even if the proposed campaign targets selected languages; requested language IDs and this limitation are returned in scope. A dated request covers its inclusive dates; an open-ended daily-budget request uses an explicit 28-day planning window. Reach and impressions are provider estimates at the returned forecast spend, not guarantees or an exact configured-campaign forecast. Reach and impressions use native on-target fields only; missing on-target fields remain null with no fallback to totals. Google may privacy-round low reach to zero. Unknown metrics are null, distinct from Google’s returned zero. No campaign mutation or spend.
curl --request POST \
--url https://app.sideshift.app/api/oauth/v1/ads/google/forecast \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"connectionId": "<string>",
"customerId": "<string>",
"currencyCode": "<string>",
"startDate": "<string>",
"budget": {
"amountMicros": "<string>"
},
"bidding": {
"strategy": "maximize_clicks"
},
"locationIds": [
"<string>"
],
"languageIds": [
"<string>"
],
"placements": [],
"endDate": "<string>",
"goals": {
"mode": "account"
},
"audience": {
"mode": "all"
}
}
'import requests
url = "https://app.sideshift.app/api/oauth/v1/ads/google/forecast"
payload = {
"connectionId": "<string>",
"customerId": "<string>",
"currencyCode": "<string>",
"startDate": "<string>",
"budget": { "amountMicros": "<string>" },
"bidding": { "strategy": "maximize_clicks" },
"locationIds": ["<string>"],
"languageIds": ["<string>"],
"placements": [],
"endDate": "<string>",
"goals": { "mode": "account" },
"audience": { "mode": "all" }
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
connectionId: '<string>',
customerId: '<string>',
currencyCode: '<string>',
startDate: '<string>',
budget: {amountMicros: '<string>'},
bidding: {strategy: 'maximize_clicks'},
locationIds: ['<string>'],
languageIds: ['<string>'],
placements: [],
endDate: '<string>',
goals: {mode: 'account'},
audience: {mode: 'all'}
})
};
fetch('https://app.sideshift.app/api/oauth/v1/ads/google/forecast', 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://app.sideshift.app/api/oauth/v1/ads/google/forecast",
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([
'connectionId' => '<string>',
'customerId' => '<string>',
'currencyCode' => '<string>',
'startDate' => '<string>',
'budget' => [
'amountMicros' => '<string>'
],
'bidding' => [
'strategy' => 'maximize_clicks'
],
'locationIds' => [
'<string>'
],
'languageIds' => [
'<string>'
],
'placements' => [
],
'endDate' => '<string>',
'goals' => [
'mode' => 'account'
],
'audience' => [
'mode' => 'all'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"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://app.sideshift.app/api/oauth/v1/ads/google/forecast"
payload := strings.NewReader("{\n \"connectionId\": \"<string>\",\n \"customerId\": \"<string>\",\n \"currencyCode\": \"<string>\",\n \"startDate\": \"<string>\",\n \"budget\": {\n \"amountMicros\": \"<string>\"\n },\n \"bidding\": {\n \"strategy\": \"maximize_clicks\"\n },\n \"locationIds\": [\n \"<string>\"\n ],\n \"languageIds\": [\n \"<string>\"\n ],\n \"placements\": [],\n \"endDate\": \"<string>\",\n \"goals\": {\n \"mode\": \"account\"\n },\n \"audience\": {\n \"mode\": \"all\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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://app.sideshift.app/api/oauth/v1/ads/google/forecast")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"connectionId\": \"<string>\",\n \"customerId\": \"<string>\",\n \"currencyCode\": \"<string>\",\n \"startDate\": \"<string>\",\n \"budget\": {\n \"amountMicros\": \"<string>\"\n },\n \"bidding\": {\n \"strategy\": \"maximize_clicks\"\n },\n \"locationIds\": [\n \"<string>\"\n ],\n \"languageIds\": [\n \"<string>\"\n ],\n \"placements\": [],\n \"endDate\": \"<string>\",\n \"goals\": {\n \"mode\": \"account\"\n },\n \"audience\": {\n \"mode\": \"all\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.sideshift.app/api/oauth/v1/ads/google/forecast")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"connectionId\": \"<string>\",\n \"customerId\": \"<string>\",\n \"currencyCode\": \"<string>\",\n \"startDate\": \"<string>\",\n \"budget\": {\n \"amountMicros\": \"<string>\"\n },\n \"bidding\": {\n \"strategy\": \"maximize_clicks\"\n },\n \"locationIds\": [\n \"<string>\"\n ],\n \"languageIds\": [\n \"<string>\"\n ],\n \"placements\": [],\n \"endDate\": \"<string>\",\n \"goals\": {\n \"mode\": \"account\"\n },\n \"audience\": {\n \"mode\": \"all\"\n }\n}"
response = http.request(request)
puts response.read_body{
"data": {
"provider": "google",
"source": "google_reach_plan",
"status": "ready",
"currencyCode": "<string>",
"window": {
"startDate": "<string>",
"endDate": "<string>",
"planningHorizon": true
},
"scope": {
"requestedLanguageIds": [
"<string>"
],
"modeledLanguages": "all",
"metricBasis": "on_target",
"limitation": "All languages — Google forecasts do not model language targeting."
},
"reach": 4503599627370495,
"impressions": 4503599627370495,
"clicks": 4503599627370495,
"forecastSpendMicros": "<string>",
"reason": "access_required",
"message": "<string>",
"conversionRateSource": "google_suggestion",
"conversionRate": 123,
"conversionRateModel": "CUSTOMER_HISTORY",
"productCode": "<string>"
}
}{
"error": {
"code": "unauthorized",
"message": "Missing bearer access token",
"requestId": "req_..."
}
}{
"error": {
"code": "insufficient_scope",
"message": "Requires scope 'campaigns:write'",
"requestId": "req_..."
}
}{
"error": {
"code": "not_found",
"message": "Resource not found",
"requestId": "req_..."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded",
"requestId": "req_..."
}
}Authorizations
OAuth 2.1 authorization code + PKCE (S256). Tokens are tenant-bound (company_id) and scoped. Discover endpoints via /.well-known/oauth-authorization-server.
- Authorization URL
- https://app.sideshift.app/api/oauth/v1/authorize
- Token URL
- https://app.sideshift.app/api/oauth/v1/token
- Refresh URL
- https://app.sideshift.app/api/oauth/v1/token
Body
1 - 200^\d+$^[A-Z]{3}$Show child attributes
Show child attributes
- Option 1
- Option 2
- Option 3
- Option 4
Show child attributes
Show child attributes
1 - 25 elements^\d+$1 - 100 elements^\d+$1 - 7 elementsshorts, in_stream, in_feed, discover, gmail, display, maps Account defaults, standard campaign selection, immutable inherited_custom for existing campaigns, or explicit custom for a new settings-copy campaign. New custom mode reuses an enabled existing shared custom goal and freezes its account, membership, fingerprint and complete standard goal universe. It never creates or edits the shared goal; later native membership changes affect both campaigns and require review before enabling.
- Option 1
- Option 2
- Option 3
- Option 4
Show child attributes
Show child attributes
- Option 1
- Option 2
- Option 3
Show child attributes
Show child attributes
Response
data contains status ready or unavailable. Unavailable reason distinguishes access_required, no_compatible_product, invalid_targeting, conversion_rate_required, currency_mismatch, invalid_budget, malformed_response and transient_failure. Ready metrics can be null or zero.
Show child attributes
Show child attributes