Error Reference

Complete reference for all API error codes, their meanings, and how to handle them.

Error format

All API errors follow a consistent JSON structure:

{
  "error": {
    "code": "QUOTA_EXCEEDED",
    "message": "Monthly address limit reached. Top up credits or upgrade your plan.",
    "upgrade_url": "https://oneshotemail.com/pricing"
  }
}
FieldTypeDescription
codestringMachine-readable error code. Use this for programmatic handling.
messagestringHuman-readable description.
upgrade_urlstringPresent only on QUOTA_EXCEEDED errors. Links to the pricing page.

Error codes by HTTP status

400 Bad Request

VALIDATION_ERROR

The request body is malformed or contains invalid values.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "ttl_seconds must be between 60 and 86400."
  }
}

Common causes:

  • ttl_seconds is out of range for your plan.
  • Missing required fields in the send object.
  • Invalid email address format in send.to.
  • Attachment content_base64 is not valid base64.

How to handle: Fix the request. This is not retryable.


401 Unauthorized

UNAUTHORIZED

The API key is missing, invalid, or has been revoked.

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or missing API key."
  }
}

Common causes:

  • No Authorization header.
  • Typo in the API key.
  • Using a revoked key (regenerated from account settings).

How to handle: Check your API key configuration. This is not retryable.

SDK exceptions:

  • Python: UnauthorizedError
  • Go: *APIError with StatusCode == 401
  • Ruby: OneShot::UnauthorizedError
  • JavaScript: UnauthorizedError
  • Java: OneShotException with statusCode == 401

402 Payment Required

QUOTA_EXCEEDED

Your monthly allocation is exhausted and you have no remaining credits.

{
  "error": {
    "code": "QUOTA_EXCEEDED",
    "message": "Monthly address limit reached. Top up credits or upgrade your plan.",
    "upgrade_url": "https://oneshotemail.com/pricing"
  }
}

How to handle:

  1. Purchase credit packs via POST /account/credits or the CLI (oneshot credits buy).
  2. Upgrade your plan.
  3. Wait for the billing cycle to reset (check billing_cycle_ends in your account info).

SDK exceptions:

  • Python: QuotaExceededError (has upgrade_url attribute)
  • Go: *APIError with StatusCode == 402 and UpgradeURL field
  • Ruby: OneShot::QuotaExceededError
  • JavaScript: QuotaExceededError (has upgradeUrl property)
  • Java: OneShotException with statusCode == 402

404 Not Found

NOT_FOUND

The requested resource does not exist.

{
  "error": {
    "code": "NOT_FOUND",
    "message": "Address not found."
  }
}

Contexts where this occurs:

  • GET /addresses/{id} — The address ID does not exist.
  • GET /addresses/{id}/email — The address exists but no email has arrived yet (status is waiting).
  • GET /addresses/{id}/email/attachments/{index} — The attachment index is out of bounds.

How to handle:

  • For address lookups: verify the address ID is correct.
  • For email retrieval: the email has not arrived yet. Use wait_for_email() instead of polling manually.

SDK exceptions:

  • Python: NotFoundError
  • Go: *APIError with StatusCode == 404
  • Ruby: OneShot::NotFoundError
  • JavaScript: NotFoundError
  • Java: OneShotException with statusCode == 404

410 Gone

EXPIRED

The address has expired and all associated data has been permanently deleted.

{
  "error": {
    "code": "EXPIRED",
    "message": "This address has expired and been deleted."
  }
}

How to handle: The data is gone. Create a new address and retry the operation that triggers the email.

SDK exceptions:

  • Python: ExpiredError
  • Go: *APIError with StatusCode == 410
  • Ruby: OneShot::ExpiredError
  • JavaScript: ExpiredError
  • Java: OneShotException with statusCode == 410

422 Unprocessable Entity

VALIDATION_ERROR

Semantically invalid request (the JSON is well-formed but the values are invalid).

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email size exceeds your plan limit of 5 MB."
  }
}

Common causes:

  • Email or attachment exceeds plan size limits.
  • TTL exceeds plan maximum.

How to handle: Adjust the request to comply with your plan limits, or upgrade.


429 Too Many Requests

RATE_LIMITED

You have exceeded the rate limit for your plan.

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Retry after 2 seconds."
  }
}

Response headers:

HeaderDescription
Retry-AfterSeconds to wait before retrying.

How to handle: Wait for the duration specified in Retry-After, then retry. Use exponential backoff with jitter if you are making concurrent requests.

SDK exceptions:

  • Python: RateLimitedError (has retry_after attribute)
  • Go: *APIError with StatusCode == 429
  • Ruby: OneShot::RateLimitedError
  • JavaScript: RateLimitedError
  • Java: OneShotException with statusCode == 429

Retryable vs non-retryable errors

CodeRetryableStrategy
VALIDATION_ERRORNoFix the request.
UNAUTHORIZEDNoFix the API key.
QUOTA_EXCEEDEDNoBuy credits, upgrade, or wait for cycle reset.
NOT_FOUNDMaybeIf waiting for email, poll again. Otherwise, not retryable.
EXPIREDNoCreate a new address.
RATE_LIMITEDYesWait Retry-After seconds, then retry.
5xx errorsYesRetry with exponential backoff (max 3 attempts).

SDK error handling examples

Python

import oneshot
from oneshot.exceptions import (
    NotFoundError,
    ExpiredError,
    QuotaExceededError,
    RateLimitedError,
    WaitTimeoutError,
)

try:
    email = oneshot.wait_for_email(addr.id, timeout=30)
except WaitTimeoutError:
    print("Email did not arrive in time. Check your app's email delivery.")
except ExpiredError:
    print("Address expired before the email arrived. Increase TTL.")
except QuotaExceededError as e:
    print(f"Out of quota. Upgrade at: {e.upgrade_url}")
except RateLimitedError as e:
    print(f"Rate limited. Retry after {e.retry_after} seconds.")

Go

email, err := client.WaitForEmail(ctx, addr.ID, &oneshot.WaitOptions{
    Timeout: 30 * time.Second,
})
if err != nil {
    var apiErr *oneshot.APIError
    if errors.As(err, &apiErr) {
        switch apiErr.StatusCode {
        case 402:
            log.Fatalf("Quota exceeded: %s", apiErr.Message)
        case 410:
            log.Fatalf("Address expired: %s", apiErr.Message)
        case 429:
            log.Printf("Rate limited, retrying...")
        }
    }
    log.Fatal(err)
}

JavaScript

import { OneShotClient, QuotaExceededError, RateLimitedError } from "oneshot-mail";

try {
  const email = await client.waitForEmail(addr.id, { timeout: 30000 });
} catch (err) {
  if (err instanceof QuotaExceededError) {
    console.error(`Quota exceeded. Upgrade: ${err.upgradeUrl}`);
  } else if (err instanceof RateLimitedError) {
    console.log("Rate limited, retrying...");
  } else {
    throw err;
  }
}

Ruby

begin
  email = client.wait_for_email(addr["id"], timeout: 30)
rescue OneShot::QuotaExceededError
  puts "Quota exceeded. Please upgrade your plan."
rescue OneShot::TimeoutError
  puts "Email did not arrive within 30 seconds."
rescue OneShot::ExpiredError
  puts "Address expired before receiving an email."
end

Java

try {
    Email email = client.waitForEmail(addr.id, Duration.ofSeconds(30));
} catch (OneShotException e) {
    switch (e.getStatusCode()) {
        case 402 -> System.err.println("Quota exceeded: " + e.getMessage());
        case 410 -> System.err.println("Address expired: " + e.getMessage());
        case 429 -> System.out.println("Rate limited, retrying...");
        default -> throw e;
    }
}