curl --request POST \
--url https://app.sideshift.app/api/oauth/v1/invoices \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"customerEmail": "client@example.com",
"lineItems": [
{
"description": "Consulting",
"amountCents": 50000
}
]
}
'import requests
url = "https://app.sideshift.app/api/oauth/v1/invoices"
payload = {
"customerEmail": "client@example.com",
"lineItems": [
{
"description": "Consulting",
"amountCents": 50000
}
]
}
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({
customerEmail: 'client@example.com',
lineItems: [{description: 'Consulting', amountCents: 50000}]
})
};
fetch('https://app.sideshift.app/api/oauth/v1/invoices', 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/invoices",
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([
'customerEmail' => 'client@example.com',
'lineItems' => [
[
'description' => 'Consulting',
'amountCents' => 50000
]
]
]),
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/invoices"
payload := strings.NewReader("{\n \"customerEmail\": \"client@example.com\",\n \"lineItems\": [\n {\n \"description\": \"Consulting\",\n \"amountCents\": 50000\n }\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/invoices")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"customerEmail\": \"client@example.com\",\n \"lineItems\": [\n {\n \"description\": \"Consulting\",\n \"amountCents\": 50000\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.sideshift.app/api/oauth/v1/invoices")
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 \"customerEmail\": \"client@example.com\",\n \"lineItems\": [\n {\n \"description\": \"Consulting\",\n \"amountCents\": 50000\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"data": {
"id": "<string>",
"invoiceNumber": "<unknown>",
"status": "<unknown>",
"customerEmail": "<unknown>",
"currency": "<unknown>",
"totalCents": "<unknown>",
"hostedInvoiceUrl": "<unknown>",
"pdfUrl": "<unknown>",
"createdAt": "<unknown>"
},
"url": "<string>"
}{
"error": {
"code": "invalid_request",
"message": "name is required",
"requestId": "req_..."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing bearer access token",
"requestId": "req_..."
}
}{
"error": {
"code": "subscription_required",
"message": "The company does not have an active subscription",
"requestId": "req_..."
}
}{
"error": {
"code": "forbidden",
"message": "payouts:write is not available for sandbox (test-mode) grants",
"requestId": "req_..."
}
}{
"error": {
"code": "idempotency_conflict",
"message": "Idempotency-Key was reused with a different request body",
"requestId": "req_..."
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded",
"requestId": "req_..."
}
}Create + send an invoice
Create and email an invoice for the token tenant. invoices:write, sandbox-rejected (fires real Stripe, payment-provider, and Resend side effects). The payload is validated/normalized and gated (subscription + invoicing availability) server-side. Accounts that have not been granted invoicing access and that have not reached the agency payout-volume threshold are rejected with 403 forbidden. When the invoice allows BNPL rails (klarna, affirm, afterpay_clearpay, sezzle, zip, card installments, and more), the hostedInvoiceUrl page offers a Pay now / Pay over time picker; BNPL is processed by Whop. The payer total is unchanged, but the merchant’s net proceeds are reduced by a 16% merchant fee when the payer pays over time. BNPL options appear only when the provider is enabled for the account, supports the invoice currency, and the invoice total is within the provider’s supported amount range. Renewal invoices do not offer BNPL.
curl --request POST \
--url https://app.sideshift.app/api/oauth/v1/invoices \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"customerEmail": "client@example.com",
"lineItems": [
{
"description": "Consulting",
"amountCents": 50000
}
]
}
'import requests
url = "https://app.sideshift.app/api/oauth/v1/invoices"
payload = {
"customerEmail": "client@example.com",
"lineItems": [
{
"description": "Consulting",
"amountCents": 50000
}
]
}
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({
customerEmail: 'client@example.com',
lineItems: [{description: 'Consulting', amountCents: 50000}]
})
};
fetch('https://app.sideshift.app/api/oauth/v1/invoices', 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/invoices",
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([
'customerEmail' => 'client@example.com',
'lineItems' => [
[
'description' => 'Consulting',
'amountCents' => 50000
]
]
]),
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/invoices"
payload := strings.NewReader("{\n \"customerEmail\": \"client@example.com\",\n \"lineItems\": [\n {\n \"description\": \"Consulting\",\n \"amountCents\": 50000\n }\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/invoices")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"customerEmail\": \"client@example.com\",\n \"lineItems\": [\n {\n \"description\": \"Consulting\",\n \"amountCents\": 50000\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.sideshift.app/api/oauth/v1/invoices")
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 \"customerEmail\": \"client@example.com\",\n \"lineItems\": [\n {\n \"description\": \"Consulting\",\n \"amountCents\": 50000\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"data": {
"id": "<string>",
"invoiceNumber": "<unknown>",
"status": "<unknown>",
"customerEmail": "<unknown>",
"currency": "<unknown>",
"totalCents": "<unknown>",
"hostedInvoiceUrl": "<unknown>",
"pdfUrl": "<unknown>",
"createdAt": "<unknown>"
},
"url": "<string>"
}{
"error": {
"code": "invalid_request",
"message": "name is required",
"requestId": "req_..."
}
}{
"error": {
"code": "unauthorized",
"message": "Missing bearer access token",
"requestId": "req_..."
}
}{
"error": {
"code": "subscription_required",
"message": "The company does not have an active subscription",
"requestId": "req_..."
}
}{
"error": {
"code": "forbidden",
"message": "payouts:write is not available for sandbox (test-mode) grants",
"requestId": "req_..."
}
}{
"error": {
"code": "idempotency_conflict",
"message": "Idempotency-Key was reused with a different request body",
"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.
Headers
Client-chosen key; replaying the same key + body returns the original response, a different body returns 409.
Body
Invoice payload; validated + normalized server-side. Set invoiceType: 'payment_link' to create a shareable checkout link instead: no customer fields, no email — share the returned url/hostedInvoiceUrl. (mirrors the v1 create body). Permissive.