diff --git a/client.go b/client.go
index abfb849f..969b8a20 100644
--- a/client.go
+++ b/client.go
@@ -44,7 +44,7 @@ const (
 )
 
 // APIObject represents generic api json response that is shared by most
-// domain object (like escalation
+// domain objects (like escalation)
 type APIObject struct {
 	ID      string `json:"id,omitempty"`
 	Type    string `json:"type,omitempty"`
@@ -90,13 +90,15 @@ type APIErrorObject struct {
 // While the PagerDuty REST API is documented to always return the error object,
 // we assume it's possible in exceptional failure modes for this to be omitted.
 // As such, this wrapper type provides us a way to check if the object was
-// provided while avoiding cosnumers accidentally missing a nil pointer check,
+// provided while avoiding consumers accidentally missing a nil pointer check,
 // thus crashing their whole program.
 type NullAPIErrorObject struct {
 	Valid       bool
 	ErrorObject APIErrorObject
 }
 
+var _ json.Unmarshaler = &NullAPIErrorObject{} // assert that it satisfies the json.Unmarshaler interface.
+
 // UnmarshalJSON satisfies encoding/json.Unmarshaler
 func (n *NullAPIErrorObject) UnmarshalJSON(data []byte) error {
 	var aeo APIErrorObject
@@ -135,6 +137,8 @@ type APIError struct {
 	message string
 }
 
+var _ error = &APIError{} // assert that it implements the error interface.
+
 // Error satisfies the error interface, and should contain the StatusCode,
 // APIErrorObject.Message, and APIErrorObject.Code.
 func (a APIError) Error() string {
@@ -165,7 +169,7 @@ func (a APIError) Error() string {
 func apiErrorsDetailString(errs []string) string {
 	switch n := len(errs); n {
 	case 0:
-		panic("errs slice is empty")
+		return ""
 
 	case 1:
 		return errs[0]
@@ -319,7 +323,7 @@ const (
 	DebugCaptureLastRequest DebugFlag = 1 << 0
 
 	// DebugCaptureLastResponse captures the last HTTP response from the API (if
-	// there was one) and makes it available via the LastAPIReponse() method.
+	// there was one) and makes it available via the LastAPIResponse() method.
 	//
 	// This may increase memory usage / GC, as we'll be making a copy of the
 	// full HTTP response body on each request and capturing it for inspection.
@@ -546,7 +550,7 @@ func (c *Client) decodeJSON(resp *http.Response, payload interface{}) error {
 
 func (c *Client) checkResponse(resp *http.Response, err error) (*http.Response, error) {
 	if err != nil {
-		return resp, fmt.Errorf("Error calling the API endpoint: %v", err)
+		return resp, fmt.Errorf("error calling the API endpoint: %v", err)
 	}
 
 	if resp.StatusCode < 200 || resp.StatusCode > 299 {
diff --git a/event_v2.go b/event_v2.go
index 1408362c..f7661af2 100644
--- a/event_v2.go
+++ b/event_v2.go
@@ -51,6 +51,106 @@ func ManageEvent(e V2Event) (*V2EventResponse, error) {
 	return ManageEventWithContext(context.Background(), e)
 }
 
+// EventsAPIV2Error represents the error response received when an Events API V2 call fails. The
+// HTTP response code is set inside of the StatusCode field, with the EventsAPIV2Error
+// field being the wrapper around the JSON error object returned from the Events API V2.
+//
+// This type also provides some helper methods like .BadRequest(), .RateLimited(),
+// and .Temporary() to help callers reason about how to handle the error.
+type EventsAPIV2Error struct {
+	// StatusCode is the HTTP response status code.
+	StatusCode int `json:"-"`
+
+	// EventsAPIV2Error represents the object returned by the API when an error occurs,
+	// which includes messages that should hopefully provide useful context
+	// to the end user.
+	//
+	// If the API response did not contain an error object, the .Valid field of
+	// EventsAPIV2Error will be false. If .Valid is true, the .ErrorObject field is
+	// valid and should be consulted.
+	EventsAPIV2Error NullEventsAPIV2ErrorObject
+
+	message string
+}
+
+var _ error = &EventsAPIV2Error{}            // assert that it satisfies the error interface.
+var _ json.Unmarshaler = &EventsAPIV2Error{} // assert that it satisfies the json.Unmarshaler interface.
+
+// Error satisfies the error interface, and should contain the StatusCode,
+// EventsAPIV2Error.Message, EventsAPIV2Error.ErrorObject.Status, and EventsAPIV2Error.Errors.
+func (e EventsAPIV2Error) Error() string {
+	if len(e.message) > 0 {
+		return e.message
+	}
+
+	if !e.EventsAPIV2Error.Valid {
+		return fmt.Sprintf("HTTP response failed with status code %d and no JSON error object was present", e.StatusCode)
+	}
+
+	return fmt.Sprintf(
+		"HTTP response failed with status code: %d, message: %s, status: %s: %s",
+		e.StatusCode,
+		e.EventsAPIV2Error.ErrorObject.Message,
+		e.EventsAPIV2Error.ErrorObject.Status,
+		apiErrorsDetailString(e.EventsAPIV2Error.ErrorObject.Errors),
+	)
+}
+
+// UnmarshalJSON satisfies encoding/json.Unmarshaler.
+func (e *EventsAPIV2Error) UnmarshalJSON(data []byte) error {
+	var eaeo EventsAPIV2ErrorObject
+	if err := json.Unmarshal(data, &eaeo); err != nil {
+		return err
+	}
+
+	e.EventsAPIV2Error.ErrorObject = eaeo
+	e.EventsAPIV2Error.Valid = true
+
+	return nil
+}
+
+// BadRequest returns whether the event request was rejected by PagerDuty as an
+// incorrect or invalid event structure.
+func (e EventsAPIV2Error) BadRequest() bool {
+	return e.StatusCode == http.StatusBadRequest
+}
+
+// RateLimited returns whether the response had e status of 429, and as such the
+// client is rate limited. The PagerDuty rate limits should reset once per
+// minute, and for the REST API they are an account-wide rate limit (not per
+// API key or IP).
+func (e EventsAPIV2Error) RateLimited() bool {
+	return e.StatusCode == http.StatusTooManyRequests
+}
+
+// Temporary returns whether it was a temporary error, one of which is a
+// RateLimited error.
+func (e EventsAPIV2Error) Temporary() bool {
+	return e.RateLimited() || (e.StatusCode >= 500 && e.StatusCode < 600)
+}
+
+// NullEventsAPIV2ErrorObject is a wrapper around the EventsAPIV2ErrorObject type. If the Valid
+// field is true, the API response included a structured error JSON object. This
+// structured object is then set on the ErrorObject field.
+//
+// We assume it's possible in exceptional failure modes for error objects to be omitted by PagerDuty.
+// As such, this wrapper type provides us a way to check if the object was
+// provided while avoiding consumers accidentally missing a nil pointer check,
+// thus crashing their whole program.
+type NullEventsAPIV2ErrorObject struct {
+	Valid       bool
+	ErrorObject EventsAPIV2ErrorObject
+}
+
+// EventsAPIV2ErrorObject represents the object returned by the Events V2 API when an error
+// occurs. This includes messages that should hopefully provide useful context
+// to the end user.
+type EventsAPIV2ErrorObject struct {
+	Status  string   `json:"status,omitempty"`
+	Message string   `json:"message,omitempty"`
+	Errors  []string `json:"errors,omitempty"`
+}
+
 // ManageEventWithContext handles the trigger, acknowledge, and resolve methods for an event.
 func ManageEventWithContext(ctx context.Context, e V2Event) (*V2EventResponse, error) {
 	data, err := json.Marshal(e)
@@ -73,21 +173,33 @@ func ManageEventWithContext(ctx context.Context, e V2Event) (*V2EventResponse, e
 	}
 
 	defer func() { _ = resp.Body.Close() }() // explicitly discard error
-
 	if resp.StatusCode != http.StatusAccepted {
-		bytes, err := ioutil.ReadAll(resp.Body)
+		b, err := ioutil.ReadAll(resp.Body)
 		if err != nil {
-			return nil, fmt.Errorf("HTTP Status Code: %d", resp.StatusCode)
+			return nil, EventsAPIV2Error{
+				StatusCode: resp.StatusCode,
+				message:    fmt.Sprintf("HTTP response with status code: %d: error: %s", resp.StatusCode, err),
+			}
+		}
+		// now try to decode the response body into the error object.
+		var eae EventsAPIV2Error
+		err = json.Unmarshal(b, &eae)
+		if err != nil {
+			eae = EventsAPIV2Error{
+				StatusCode: resp.StatusCode,
+				message:    fmt.Sprintf("HTTP response with status code: %d, JSON unmarshal object body failed: %s, body: %s", resp.StatusCode, err, string(b)),
+			}
+			return nil, eae
 		}
 
-		return nil, fmt.Errorf("HTTP Status Code: %d, Message: %s", resp.StatusCode, string(bytes))
+		eae.StatusCode = resp.StatusCode
+		return nil, eae
 	}
 
 	var eventResponse V2EventResponse
 	if err := json.NewDecoder(resp.Body).Decode(&eventResponse); err != nil {
 		return nil, err
 	}
-
 	return &eventResponse, nil
 }
 
diff --git a/event_v2_test.go b/event_v2_test.go
index d590959c..30095cd0 100644
--- a/event_v2_test.go
+++ b/event_v2_test.go
@@ -32,3 +32,178 @@ func TestEventV2_ManageEvent(t *testing.T) {
 
 	testEqual(t, want, res)
 }
+
+func TestEventsAPIV2Error_BadRequest(t *testing.T) {
+	tests := []struct {
+		name string
+		e    EventsAPIV2Error
+		want bool
+	}{
+		{
+			name: "bad_request",
+			e: EventsAPIV2Error{
+				StatusCode: http.StatusBadRequest,
+				EventsAPIV2Error: NullEventsAPIV2ErrorObject{
+					Valid: true,
+					ErrorObject: EventsAPIV2ErrorObject{
+						Status:  "invalid",
+						Message: "Event object is invalid",
+						Errors:  []string{"Length of 'routing_key' is incorrect (should be 32 characters)", "'event_action' is missing or blank"},
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "rate_limited",
+			e: EventsAPIV2Error{
+				StatusCode: http.StatusTooManyRequests,
+				EventsAPIV2Error: NullEventsAPIV2ErrorObject{
+					Valid: true,
+					ErrorObject: EventsAPIV2ErrorObject{
+						Status:  "throttle exceeded",
+						Message: "Requests for this service are arriving too quickly.  Please retry later.",
+						Errors:  []string{"Enhance Your Calm."},
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "InternalServerError",
+			e: EventsAPIV2Error{
+				StatusCode: http.StatusInternalServerError,
+			},
+			want: false,
+		},
+		{
+			name: "ServiceUnavailable",
+			e: EventsAPIV2Error{
+				StatusCode: http.StatusServiceUnavailable,
+			},
+			want: false,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.e.BadRequest(); got != tt.want {
+				t.Fatalf("tt.e.BadRequest() = %t, want %t", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestEventsAPIV2Error_RateLimited(t *testing.T) {
+	tests := []struct {
+		name string
+		e    EventsAPIV2Error
+		want bool
+	}{
+		{
+			name: "rate_limited",
+			e: EventsAPIV2Error{
+				StatusCode: http.StatusTooManyRequests,
+				EventsAPIV2Error: NullEventsAPIV2ErrorObject{
+					Valid: true,
+					ErrorObject: EventsAPIV2ErrorObject{
+						Status:  "throttle exceeded",
+						Message: "Requests for this service are arriving too quickly.  Please retry later.",
+						Errors:  []string{"Enhance Your Calm"},
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "not_found",
+			e: EventsAPIV2Error{
+				StatusCode: http.StatusNotFound,
+				EventsAPIV2Error: NullEventsAPIV2ErrorObject{
+					Valid: true,
+					ErrorObject: EventsAPIV2ErrorObject{
+						Status:  "Not Found",
+						Message: "Not Found",
+						Errors:  []string{"Not Found"},
+					},
+				},
+			},
+			want: false,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.e.RateLimited(); got != tt.want {
+				t.Fatalf("tt.e.RateLimited() = %t, want %t", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestEventsAPIV2Error_Temporary(t *testing.T) {
+	tests := []struct {
+		name string
+		e    EventsAPIV2Error
+		want bool
+	}{
+		{
+			name: "rate_limited",
+			e: EventsAPIV2Error{
+				StatusCode: http.StatusTooManyRequests,
+				EventsAPIV2Error: NullEventsAPIV2ErrorObject{
+					Valid: true,
+					ErrorObject: EventsAPIV2ErrorObject{
+						Status:  "throttle exceeded",
+						Message: "Requests for this service are arriving too quickly.  Please retry later.",
+						Errors:  []string{"Enhance Your Calm"},
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "InternalServerError",
+			e: EventsAPIV2Error{
+				StatusCode: http.StatusInternalServerError,
+			},
+			want: true,
+		},
+		{
+			name: "ServiceUnavailable",
+			e: EventsAPIV2Error{
+				StatusCode: http.StatusServiceUnavailable,
+			},
+			want: true,
+		},
+		{
+			name: "not_found",
+			e: EventsAPIV2Error{
+				StatusCode: http.StatusNotFound,
+				EventsAPIV2Error: NullEventsAPIV2ErrorObject{
+					Valid: true,
+					ErrorObject: EventsAPIV2ErrorObject{
+						Status:  "Not Found",
+						Message: "Not Found",
+						Errors:  []string{"Not Found"},
+					},
+				},
+			},
+			want: false,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.e.Temporary(); got != tt.want {
+				t.Fatalf("tt.e.Temporary() = %t, want %t", got, tt.want)
+			}
+		})
+	}
+}