ShipPulse
  • Pricing
  • Docs
  • Blog
  • Compare

Product

TestimonialsChangelogStatus PagesFeedbackRoadmapPricing

Resources

DocsBlogAPI ReferenceSDKHelp Center

Company

AboutContact

Legal

TermsPrivacyCookie PolicyDPASub-processors

Product updates

Changelog updates only. Unsubscribe any time.

ShipPulse operated by Igor Bogdanov, Limassol, Cyprus. [email protected]. Cyprus registration number pending — will be published once issued.

ShipPulse

© 2026 ShipPulse. All rights reserved.

OverviewQuick StartCore ConceptsWidget ReferencePlatform GuidesAPI ReferenceAPI PlaygroundError HandlingPaginationRate LimitingJavaScript SDKWebhooksZapier, n8n & MakeCustom DomainsTeam ManagementBilling & PlansNotification ChannelsAI FeaturesOverviewFrom SenjaFrom Testimonial.toFrom HeadwayFrom Canny

Introduction

  • Overview
  • Quick Start
  • Core Concepts

Embed Widgets

  • Widget Reference
  • Platform Guides

REST API

  • API Reference
  • API Playground
  • Error Handling
  • Pagination
  • Rate Limiting

SDK & Webhooks

  • JavaScript SDK
  • Webhooks
  • Zapier, n8n & Make

Guides

  • Custom Domains
  • Team Management
  • Billing & Plans
  • Notification Channels
  • AI Features

Migrations

  • Overview
  • From Senja
  • From Testimonial.to
  • From Headway
  • From Canny

Need help?

[email protected]
API

Error Handling

Every ShipPulse API error uses a consistent JSON schema. This guide covers HTTP codes, the error object structure, and recommended retry strategies.

Error response schema

All non-2xx responses return a JSON body with the following structure:

json
{
  "error": {
    "code": "validation_failed",
    "message": "Human-readable description of the error",
    "details": [
      {
        "field": "title",
        "message": "Title is required"
      }
    ]
  }
}
FieldTypeDescription
error.codestringMachine-readable error code (snake_case)
error.messagestringHuman-readable message, safe to display to users
error.detailsarray | nullField-level errors for validation failures (422)
error.details[].fieldstringThe field path that failed validation
error.details[].messagestringWhat went wrong with that field

HTTP status codes

400Bad Request
don't retry

The request body is malformed or missing required fields. Check that all required parameters are present and correctly typed.

401Unauthorized
don't retry

Missing or invalid API key. Verify your Authorization header starts with Bearer sp_ and the key has not been revoked.

403Forbidden
don't retry

Your plan does not include access to this resource. The REST API requires Pro or Agency plan. Upgrade in Settings → Billing.

404Not Found
don't retry

The requested resource does not exist or has been deleted. Check the ID in your request path.

422Unprocessable Entity
don't retry

Input passed validation format but failed business-logic rules. The response body will include a details array with field-level errors.

429Too Many Requests
safe to retry

Rate limit exceeded. Check the X-RateLimit-Reset header for when the window resets and retry after that time.

500Internal Server Error
safe to retry

An unexpected error on ShipPulse's side. Safe to retry with exponential backoff. Check https://status.shippulse.dev if errors persist.

Retry strategy

Only 429 and 5xx errors are safe to retry. Use exponential backoff with jitter to avoid thundering herd problems.

typescript
async function fetchWithRetry(
  url: string,
  options: RequestInit,
  maxRetries = 3,
): Promise<Response> {
  let attempt = 0;

  while (attempt <= maxRetries) {
    const res = await fetch(url, options);

    // Success
    if (res.ok) return res;

    // Don't retry client errors (4xx except 429)
    if (res.status >= 400 && res.status < 500 && res.status !== 429) {
      const error = await res.json();
      throw new Error(error.error?.message ?? "API error");
    }

    // Rate limit: honour Retry-After header
    if (res.status === 429) {
      const retryAfter = Number(res.headers.get("Retry-After") ?? 60);
      await sleep(retryAfter * 1000);
      attempt++;
      continue;
    }

    // 5xx: exponential backoff with jitter
    const backoff = Math.min(1000 * 2 ** attempt + Math.random() * 500, 30_000);
    await sleep(backoff);
    attempt++;
  }

  throw new Error("Max retries exceeded");
}

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

Typed error handling

Create a typed wrapper so every API call returns structured errors rather than thrown exceptions:

typescript
interface ShipPulseError {
  code: string;
  message: string;
  details?: Array<{ field: string; message: string }>;
}

interface ApiResult<T> {
  data?: T;
  error?: ShipPulseError;
}

async function apiCall<T>(url: string, options?: RequestInit): Promise<ApiResult<T>> {
  try {
    const res = await fetch(url, options);
    const json = await res.json();

    if (!res.ok) {
      return { error: json.error as ShipPulseError };
    }

    return { data: json.data as T };
  } catch (err) {
    return {
      error: { code: "network_error", message: "Failed to reach ShipPulse API" },
    };
  }
}

// Usage
const { data, error } = await apiCall<Testimonial[]>(
  "https://shippulse.dev/api/v1/testimonials",
  { headers: { Authorization: "Bearer sp_..." } },
);

if (error) {
  if (error.code === "validation_failed") {
    // Show field errors
    error.details?.forEach((d) => console.error(d.field, d.message));
  } else {
    console.error(error.message);
  }
}

Prefer the JavaScript SDK for a fully typed client with built-in error handling and retry logic.