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

  1. Create one internal work item per outbound document.
  2. Upload the PDF or reference an existing `document_key`.
  3. Create a draft shipment for that document and recipient.
  4. Select a quote and buy the shipment.
  5. Persist the Corvo shipment ID, quote ID, and tracking data in your own system.

TypeScript worker example

TypeScript
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.