curl --request POST \
--url https://sendpaperplane.com/v1/orders \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"mail_class": "first_class",
"to": {
"name": "Jordan Rivera",
"line1": "742 Evergreen Terrace",
"city": "Springfield",
"state": "IL",
"zip": "62704"
},
"from": {
"name": "Alex Chen",
"line1": "221B Baker St",
"city": "Springfield",
"state": "IL",
"zip": "62701"
}
}
'import requests
url = "https://sendpaperplane.com/v1/orders"
payload = {
"mail_class": "first_class",
"to": {
"name": "Jordan Rivera",
"line1": "742 Evergreen Terrace",
"city": "Springfield",
"state": "IL",
"zip": "62704"
},
"from": {
"name": "Alex Chen",
"line1": "221B Baker St",
"city": "Springfield",
"state": "IL",
"zip": "62701"
}
}
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({
mail_class: 'first_class',
to: {
name: 'Jordan Rivera',
line1: '742 Evergreen Terrace',
city: 'Springfield',
state: 'IL',
zip: '62704'
},
from: {
name: 'Alex Chen',
line1: '221B Baker St',
city: 'Springfield',
state: 'IL',
zip: '62701'
}
})
};
fetch('https://sendpaperplane.com/v1/orders', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
mail_class: 'first_class',
to: {
name: 'Jordan Rivera',
line1: '742 Evergreen Terrace',
city: 'Springfield',
state: 'IL',
zip: '62704'
},
from: {
name: 'Alex Chen',
line1: '221B Baker St',
city: 'Springfield',
state: 'IL',
zip: '62701'
}
})
};
fetch('https://sendpaperplane.com/v1/orders', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://sendpaperplane.com/v1/orders"
payload := strings.NewReader("{\n \"mail_class\": \"first_class\",\n \"to\": {\n \"name\": \"Jordan Rivera\",\n \"line1\": \"742 Evergreen Terrace\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip\": \"62704\"\n },\n \"from\": {\n \"name\": \"Alex Chen\",\n \"line1\": \"221B Baker St\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip\": \"62701\"\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))
}require 'uri'
require 'net/http'
url = URI("https://sendpaperplane.com/v1/orders")
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 \"mail_class\": \"first_class\",\n \"to\": {\n \"name\": \"Jordan Rivera\",\n \"line1\": \"742 Evergreen Terrace\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip\": \"62704\"\n },\n \"from\": {\n \"name\": \"Alex Chen\",\n \"line1\": \"221B Baker St\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip\": \"62701\"\n }\n}"
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sendpaperplane.com/v1/orders",
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([
'mail_class' => 'first_class',
'to' => [
'name' => 'Jordan Rivera',
'line1' => '742 Evergreen Terrace',
'city' => 'Springfield',
'state' => 'IL',
'zip' => '62704'
],
'from' => [
'name' => 'Alex Chen',
'line1' => '221B Baker St',
'city' => 'Springfield',
'state' => 'IL',
'zip' => '62701'
]
]),
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;
}HttpResponse<String> response = Unirest.post("https://sendpaperplane.com/v1/orders")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"mail_class\": \"first_class\",\n \"to\": {\n \"name\": \"Jordan Rivera\",\n \"line1\": \"742 Evergreen Terrace\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip\": \"62704\"\n },\n \"from\": {\n \"name\": \"Alex Chen\",\n \"line1\": \"221B Baker St\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip\": \"62701\"\n }\n}")
.asString();{
"status": "ok",
"order": {
"id": "ord_test_1a2b3c4d5e6f7a8b",
"status": "submitted",
"sandbox": true,
"mail_class": "first_class",
"page_count": 1,
"color": false,
"to": {
"name": "Jordan Rivera",
"city": "Springfield",
"state": "IL",
"zip": "62704"
},
"price_cents": 199,
"breakdown": [
{
"id": "first_class_letter_1_page",
"label": "First-Class letter, 1 page",
"amount_cents": 199
}
],
"tracking_number": null,
"expected_delivery_date": null,
"refusal_reason": null,
"created_at": "2026-08-23T12:00:00.000Z",
"updated_at": "2026-08-23T12:00:00.000Z"
},
"replayed": true,
"capability": {
"cancel_token": "ppc_a1b2c3d4",
"review_token": "ppc_e5f6g7h8"
}
}{
"status": "ok",
"order": {
"id": "ord_test_1a2b3c4d5e6f7a8b",
"status": "submitted",
"sandbox": true,
"mail_class": "first_class",
"page_count": 1,
"color": false,
"to": {
"name": "Jordan Rivera",
"city": "Springfield",
"state": "IL",
"zip": "62704"
},
"price_cents": 199,
"breakdown": [
{
"id": "first_class_letter_1_page",
"label": "First-Class letter, 1 page",
"amount_cents": 199
}
],
"tracking_number": null,
"expected_delivery_date": null,
"refusal_reason": null,
"created_at": "2026-08-23T12:00:00.000Z",
"updated_at": "2026-08-23T12:00:00.000Z"
},
"capability": {
"cancel_token": "ppc_a1b2c3d4",
"review_token": "ppc_e5f6g7h8"
}
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}Send a letter
Renders the document, prices it, verifies both addresses with USPS, then either charges a prepaid credit code or returns a Stripe payment link. This spends money and mails a physical object to a real address. Charges the sender once payment is captured — immediately if a credit code covers it, otherwise when the returned payment_url is paid — or spends a sandbox no-op if sandbox: true for a free, instant, end-to-end simulation. Provide exactly one of text, upload_key, pdf_base64, or pdf_url.
Send an Idempotency-Key header on every call. A repeat with the same key returns the original order with replayed: true and HTTP 200 instead of mailing a second letter, which makes a timeout safe to retry.
On the agent path a confirmation_token from POST /v1/quotes is required; without one the call fails 428 confirmation_required rather than sending.
curl --request POST \
--url https://sendpaperplane.com/v1/orders \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"mail_class": "first_class",
"to": {
"name": "Jordan Rivera",
"line1": "742 Evergreen Terrace",
"city": "Springfield",
"state": "IL",
"zip": "62704"
},
"from": {
"name": "Alex Chen",
"line1": "221B Baker St",
"city": "Springfield",
"state": "IL",
"zip": "62701"
}
}
'import requests
url = "https://sendpaperplane.com/v1/orders"
payload = {
"mail_class": "first_class",
"to": {
"name": "Jordan Rivera",
"line1": "742 Evergreen Terrace",
"city": "Springfield",
"state": "IL",
"zip": "62704"
},
"from": {
"name": "Alex Chen",
"line1": "221B Baker St",
"city": "Springfield",
"state": "IL",
"zip": "62701"
}
}
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({
mail_class: 'first_class',
to: {
name: 'Jordan Rivera',
line1: '742 Evergreen Terrace',
city: 'Springfield',
state: 'IL',
zip: '62704'
},
from: {
name: 'Alex Chen',
line1: '221B Baker St',
city: 'Springfield',
state: 'IL',
zip: '62701'
}
})
};
fetch('https://sendpaperplane.com/v1/orders', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
mail_class: 'first_class',
to: {
name: 'Jordan Rivera',
line1: '742 Evergreen Terrace',
city: 'Springfield',
state: 'IL',
zip: '62704'
},
from: {
name: 'Alex Chen',
line1: '221B Baker St',
city: 'Springfield',
state: 'IL',
zip: '62701'
}
})
};
fetch('https://sendpaperplane.com/v1/orders', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://sendpaperplane.com/v1/orders"
payload := strings.NewReader("{\n \"mail_class\": \"first_class\",\n \"to\": {\n \"name\": \"Jordan Rivera\",\n \"line1\": \"742 Evergreen Terrace\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip\": \"62704\"\n },\n \"from\": {\n \"name\": \"Alex Chen\",\n \"line1\": \"221B Baker St\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip\": \"62701\"\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))
}require 'uri'
require 'net/http'
url = URI("https://sendpaperplane.com/v1/orders")
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 \"mail_class\": \"first_class\",\n \"to\": {\n \"name\": \"Jordan Rivera\",\n \"line1\": \"742 Evergreen Terrace\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip\": \"62704\"\n },\n \"from\": {\n \"name\": \"Alex Chen\",\n \"line1\": \"221B Baker St\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip\": \"62701\"\n }\n}"
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sendpaperplane.com/v1/orders",
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([
'mail_class' => 'first_class',
'to' => [
'name' => 'Jordan Rivera',
'line1' => '742 Evergreen Terrace',
'city' => 'Springfield',
'state' => 'IL',
'zip' => '62704'
],
'from' => [
'name' => 'Alex Chen',
'line1' => '221B Baker St',
'city' => 'Springfield',
'state' => 'IL',
'zip' => '62701'
]
]),
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;
}HttpResponse<String> response = Unirest.post("https://sendpaperplane.com/v1/orders")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"mail_class\": \"first_class\",\n \"to\": {\n \"name\": \"Jordan Rivera\",\n \"line1\": \"742 Evergreen Terrace\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip\": \"62704\"\n },\n \"from\": {\n \"name\": \"Alex Chen\",\n \"line1\": \"221B Baker St\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zip\": \"62701\"\n }\n}")
.asString();{
"status": "ok",
"order": {
"id": "ord_test_1a2b3c4d5e6f7a8b",
"status": "submitted",
"sandbox": true,
"mail_class": "first_class",
"page_count": 1,
"color": false,
"to": {
"name": "Jordan Rivera",
"city": "Springfield",
"state": "IL",
"zip": "62704"
},
"price_cents": 199,
"breakdown": [
{
"id": "first_class_letter_1_page",
"label": "First-Class letter, 1 page",
"amount_cents": 199
}
],
"tracking_number": null,
"expected_delivery_date": null,
"refusal_reason": null,
"created_at": "2026-08-23T12:00:00.000Z",
"updated_at": "2026-08-23T12:00:00.000Z"
},
"replayed": true,
"capability": {
"cancel_token": "ppc_a1b2c3d4",
"review_token": "ppc_e5f6g7h8"
}
}{
"status": "ok",
"order": {
"id": "ord_test_1a2b3c4d5e6f7a8b",
"status": "submitted",
"sandbox": true,
"mail_class": "first_class",
"page_count": 1,
"color": false,
"to": {
"name": "Jordan Rivera",
"city": "Springfield",
"state": "IL",
"zip": "62704"
},
"price_cents": 199,
"breakdown": [
{
"id": "first_class_letter_1_page",
"label": "First-Class letter, 1 page",
"amount_cents": 199
}
],
"tracking_number": null,
"expected_delivery_date": null,
"refusal_reason": null,
"created_at": "2026-08-23T12:00:00.000Z",
"updated_at": "2026-08-23T12:00:00.000Z"
},
"capability": {
"cancel_token": "ppc_a1b2c3d4",
"review_token": "ppc_e5f6g7h8"
}
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}{
"status": "failed",
"code": "<string>",
"reason": "<string>",
"next": [
"<string>"
]
}Authorizations
Optional developer API key: Authorization: Bearer pp_live_… (or pp_test_… for a sandbox-only key). Omitting it is a supported way to call every route. A key that is sent and does not verify is refused with 401 rather than treated as anonymous, so a typo fails loudly instead of quietly losing the restriction you meant to apply. Enabled per deployment via SCOPED_API_KEYS.
Headers
Caller-chosen unique string. A retry with the same value replays the original order (HTTP 200, replayed: true) instead of creating a second one.
Body
first_class, certified, certified_err, priority Hide child attributes
Hide child attributes
1 - 801 - 1201 - 802^\d{5}(-\d{4})?$120"US"Hide child attributes
Hide child attributes
1 - 801 - 1201 - 802^\d{5}(-\d{4})?$120"US"5000040^up_[0-9a-f]{32}\.pdf$14000000^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$^pp-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$2000The physical piece to mail. Defaults to 'letter'. Postcards (postcard_4x6 $1.99, postcard_6x9 $2.99, postcard_6x11 $3.49) and the notecard (notecard $5.99 — a folded card, enclosed like a greeting card rather than open-face like a postcard) are message-only today: text prints on the piece itself via a handwriting engine, no custom photo or artwork side, and no PDF/upload path; printing and First-Class postage are both included in the price. All of them allow only mail_class='first_class' and carry no tracking.
letter, letter_windowed, notecard, postcard_4x6, postcard_6x9, postcard_6x11 Pay to leave the "Mailed via paperplane" mark off this envelope. Only charged where a mark would otherwise have been printed — never on Certified, Priority, or postcards, which carry no mark to begin with.
2000hosted, embedded Response
Idempotent replay. The Idempotency-Key was already used, so the original order is returned with replayed: true and nothing new was created or charged. Distinguishable from a fresh send by the status code alone.
- Option 1
- Option 2
- Option 3
ok, action_required, failed Hide child attributes
Hide child attributes
draft, pending_payment, screening, held_for_review, submitted, mailed, delivered, refused, canceled, failed first_class, certified, certified_err, priority -9007199254740991 <= x <= 9007199254740991-9007199254740991 <= x <= 9007199254740991Hide child attributes
Hide child attributes
1 - 801 - 1201 - 802^\d{5}(-\d{4})?$120"US"hosted, embedded Was this page helpful?