High-Volume Workflows
Corvo does not expose a batch shipment endpoint. High-volume integrations should queue one shipment per job and process those jobs sequentially or with carefully bounded concurrency.
Recommended pattern
- Create one internal work item per outbound document.
- Upload the PDF or reference an existing `document_key`.
- Create a draft shipment for that document and recipient.
- Select a quote and buy the shipment.
- Persist the Corvo shipment ID, quote ID, and tracking data in your own system.
No batch endpoint and no idempotency header
The public API currently does not expose a multi-shipment send endpoint or an `Idempotency-Key` header. Build dedupe and retry controls in your own queue worker.
TypeScript worker example
type QueueItem = {
jobId: string;
documentKey: string;
shipmentName: string;
toAddress: {
name?: string;
company?: string;
street1: string;
street2?: string;
city: string;
state: string;
zip: string;
country?: string;
};
};
const BASE_URL = "https://corvo.to/api/v1";
const API_KEY = process.env.CORVO_API_KEY!;
async function createDraft(item: QueueItem) {
const response = await fetch(`${BASE_URL}/shipments`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
document_key: item.documentKey,
shipment_name: item.shipmentName,
to_address: item.toAddress,
print_options: { color: false, duplex: false },
shipping_options: {
certified_mail: true,
return_receipt: true,
},
}),
});
if (!response.ok) throw new Error(`create failed: ${response.status}`);
return (await response.json()).data;
}
async function buyShipment(shipmentId: string, quoteId: string) {
const response = await fetch(`${BASE_URL}/shipments/${shipmentId}/buy`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ quote_id: quoteId }),
});
if (!response.ok) throw new Error(`buy failed: ${response.status}`);
return (await response.json()).data;
}
async function processQueue(items: QueueItem[]) {
for (const item of items) {
await withRetry(async () => {
const draft = await createDraft(item);
const quote = draft.rates[0];
const purchased = await buyShipment(draft.id, quote.quote_id);
console.log("created shipment", {
jobId: item.jobId,
shipmentId: purchased.id,
trackingNumber: purchased.tracking_number,
});
});
}
}
async function withRetry(fn: () => Promise<void>, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
try {
await fn();
return;
} catch (error) {
if (attempt === maxRetries) throw error;
const backoffMs = 2 ** attempt * 1000;
await new Promise((resolve) => setTimeout(resolve, backoffMs));
}
}
}Operational guardrails
- Store your own internal job ID and map it to the Corvo shipment ID.
- Respect `429 RATE_LIMITED` responses and the `Retry-After` header.
- Keep concurrency deliberately low for create and buy operations.
- Recreate drafts if quotes expire before you call `/buy`.
- Keep a dead-letter queue for failures that need manual review.