Contract checks

Edge cases

Safe cases should not create records. A pass means the live response matched the documented code, message, or shape.

0 pass0 fail0 review88 untested

Rate-limit probe

Sends parallel reads of the join form. The budget is 120 a minute for the whole key. Keep the count small unless you mean to trip it.

AuthenticationsafeMissing API key

A request with no Authorization header.

Expected. UNAUTHORIZED, and the message says the API key is missing.

AuthenticationsafeBlank bearer token

Authorization is present, but the token is empty.

Expected. UNAUTHORIZED.

AuthenticationsafeUnknown API key

A well-shaped key that was never issued.

Expected. UNAUTHORIZED, and the message says the API key is invalid. This is different from a deleted key.

AuthenticationsafeWrong auth scheme

Sends Token instead of Bearer.

Expected. UNAUTHORIZED.

Console guardssafePath escape is refused locally

The production key must never be attached to a path that leaves the documented API.

Expected. The console blocks the call before it leaves this app.

Console guardssafeIdempotency key with a line break

A newline in a header is request smuggling fuel, so the console refuses to send it. The API also forbids line breaks in this key.

Expected. The console blocks the header.

Join formsafeJoin form shape

The community has one form, with fields and rules, and one email field.

Expected. 200, success envelope, fields, rules, and an email field.

PaginationsafeInvitation page defaults

A small page request.

Expected. 200 and a page envelope.

Paginationsafeper_page at the maximum

100 is the documented maximum page size.

Expected. 200 and a page envelope.

Paginationsafeper_page above the maximum

101 is past the documented maximum of 100.

Expected. VALIDATION_ERROR.

Paginationsafeper_page far past the maximum

A five-digit page size should not be silently accepted.

Expected. VALIDATION_ERROR.

Paginationsafeper_page=0

Zero is not a page size.

Expected. VALIDATION_ERROR.

Paginationsafeper_page negative

A negative page size.

Expected. VALIDATION_ERROR.

Paginationsafeper_page is not a number

A word where a page size belongs.

Expected. VALIDATION_ERROR.

Paginationsafepage=0

Pages start at 1.

Expected. VALIDATION_ERROR.

Paginationsafepage negative

A negative page number.

Expected. VALIDATION_ERROR.

Paginationsafepage is not a number

A word where a page number belongs.

Expected. VALIDATION_ERROR.

PaginationsafeFractional page

page=1.5 is not an integer page.

Expected. VALIDATION_ERROR, or a documented decision to truncate. A 500 is a failure.

PaginationsafePage past the end

A very high page number should be an empty page, not an error, unless the API documents otherwise.

Expected. 200 and a page whose items are empty, or a validation error. A 500 is a failure.

Invitation filterssafeFilter status=pending

Every documented status is a legal filter, including when nothing matches.

Expected. 200 and a page of invitations.

Invitation filterssafeFilter status=awaiting_verification

Every documented status is a legal filter, including when nothing matches.

Expected. 200 and a page of invitations.

Invitation filterssafeFilter status=approved

Every documented status is a legal filter, including when nothing matches.

Expected. 200 and a page of invitations.

Invitation filterssafeFilter status=delivering

Every documented status is a legal filter, including when nothing matches.

Expected. 200 and a page of invitations.

Invitation filterssafeFilter status=sent

Every documented status is a legal filter, including when nothing matches.

Expected. 200 and a page of invitations.

Invitation filterssafeFilter status=delivery_failed

Every documented status is a legal filter, including when nothing matches.

Expected. 200 and a page of invitations.

Invitation filterssafeFilter status=rejected

Every documented status is a legal filter, including when nothing matches.

Expected. 200 and a page of invitations.

Invitation filterssafeUnknown status filter

status=not_a_status is not in the enum.

Expected. VALIDATION_ERROR. An empty 200 is worth a review, not a pass.

Invitation filterssafeStatus filter in the wrong case

PENDING is not the documented value pending.

Expected. VALIDATION_ERROR if matching is case-sensitive. Record the actual behavior.

Invitation filterssafeThere is no deleted status

Deleted invitations leave the list. The delete is a webhook, not a status.

Expected. VALIDATION_ERROR. Do not expect a deleted row.

Invitation filterssafeFilter platform=slack

slack is one of the four documented platforms.

Expected. 200 and a page.

Invitation filterssafeUnknown platform filter

sms is not a platform.

Expected. VALIDATION_ERROR.

Invitation filterssafeEmail filter with no match

An address that should not exist in the community.

Expected. 200 and a page. Items should be empty if matching is exact.

Invitation filterssafeUnknown query parameter

extra=1 should be ignored or rejected. It must not 500.

Expected. 200 page, or VALIDATION_ERROR.

PaginationsafeEvent list page

Event lists use the same page envelope and do not include lead_form.

Expected. 200 and a page.

PaginationsafeEvent per_page above the maximum

The page maximum applies to events too.

Expected. VALIDATION_ERROR.

IdempotencysafeIdempotency-Key on a read

The header is defined for invitation and applicant creates. Other calls ignore it.

Expected. 200 form response. The header does not change the read.

Invitation validationsafeEmpty invitation body

An object with none of the required fields.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeMissing mode

Every other field is present, mode is not.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeUnknown mode

mode=invite is not send or submit.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeMode in the wrong case

SEND is not send.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeMissing email

mode is submit and email is absent. The address is also invalid so a bug cannot deliver.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeEmail is not an email

not-an-email should fail before a record is stored.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeVery long email

An address hundreds of characters long.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeMissing platforms

Platforms are required.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeEmpty platforms

An empty platform list.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeUnknown platform

sms is not slack, discord, telegram, or whatsapp.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeDuplicate platform

The same platform twice. Email is also invalid, so this must not create a record either way.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeAnswers as an array

answers must be an object of strings.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeNumeric answers

Answer values are strings, not numbers.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeInvalid JSON

A truncated JSON body.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeText body marked as JSON

The content type is text/plain.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeJSON array instead of an object

The body is [].

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeIdempotency key longer than 128

The documented maximum is 128 characters. The email is also invalid.

Expected. VALIDATION_ERROR, and no invitation is created.

Invitation validationsafeIdempotency key with a space

Spaces are forbidden. The email is also invalid, so nothing should be stored.

Expected. VALIDATION_ERROR, and no invitation is created.

Not foundsafeUnknown invitation id

A 24-character hex id that does not exist.

Expected. NOT_FOUND, Invitation not found.

Not foundsafeInvitation id that is not an id

Sent as one encoded path segment, so this checks id handling rather than routing.

Expected. NOT_FOUND or VALIDATION_ERROR. A 500 is a failure.

Not foundsafeEdit a missing invitation

PATCH must not create an invitation for an unknown id.

Expected. NOT_FOUND.

Not foundsafeReject a missing invitation

Reject on an unknown id.

Expected. NOT_FOUND.

Not foundsafeRetry a missing invitation

Retry is confirmed locally because the route can deliver, and the id is synthetic.

Expected. NOT_FOUND.

Not foundsafeApprove a missing invitation

Approve is confirmed locally because the route can deliver, and the id is synthetic.

Expected. NOT_FOUND.

Not foundsafeDelete a missing invitation

The id is synthetic. Run-all never deletes an id you have saved.

Expected. NOT_FOUND. A 204 here would mean a real record was deleted.

Not foundsafeUnknown event id

A synthetic event id.

Expected. NOT_FOUND, Event not found.

Not foundsafeApplicants of a missing event

Listing registrations for an unknown event.

Expected. NOT_FOUND.

Not foundsafeUnknown applicant on a missing event

Both ids are synthetic.

Expected. NOT_FOUND.

Not foundsafeCreate an applicant on a missing event

A valid-looking body on an event that does not exist.

Expected. NOT_FOUND, and no applicant is stored.

Event validationsafeEmpty event body

Title and slug are both missing.

Expected. VALIDATION_ERROR, and no event is created.

Event validationsafeEvent slug without a title

Slug alone is not enough, and this slug is not a slug.

Expected. VALIDATION_ERROR.

Event writeswriteSlug with spaces

If this is accepted, a real event is created. Delete it from the response id.

Expected. VALIDATION_ERROR. A 201 means the slug rules are looser than a URL slug and a record now exists.

Event writeswriteTicket sales and free registration together

The contract says these cannot both be true. A 201 leaves a real event behind.

Expected. VALIDATION_ERROR.

Event writeswriteUnknown timezone

Not/AZone is not an IANA timezone. A 201 leaves a real event behind.

Expected. VALIDATION_ERROR.

Event writeswriteEnd before start

ends_at is a day before starts_at. A 201 leaves a real event behind.

Expected. VALIDATION_ERROR if the schedule is checked. Record a 201 as a product decision and delete the event.

Event writeswriteUnknown location type

hybrid is not physical or online.

Expected. VALIDATION_ERROR.

Form replacementdestroyRemove the email field

Replacing the live form without an email field. Run this only after you have loaded the current form in the playground, so you can restore it if the API wrongly accepts the body.

Expected. VALIDATION_ERROR, and a following read shows the previous form.

Form replacementdestroyEmpty form fields

An empty field list removes every question, including email, if it is accepted.

Expected. VALIDATION_ERROR.

Applicant validationwriteName and email beside data

The contract says not to send name or email next to data. Uses the fixture event.

Expected. VALIDATION_ERROR, and no applicant is created.

Applicant validationwriteApplicant body without data

An empty object on a real event.

Expected. VALIDATION_ERROR.

ConflictswriteEdit an invitation that is no longer pending

Use a sent, rejected, approved, or delivery_failed id from the fixture bar.

Expected. CONFLICT. The message says it can only be edited while pending or awaiting verification.

ConflictsdeliverRetry an invitation that has not failed

If the fixture invitation is delivery_failed, this will deliver again. Use a pending or sent id to expect a conflict.

Expected. CONFLICT unless the status is delivery_failed.

ManualsafeDeleted key is distinct from an unknown key

Do this with a spare key, not the only key this console is using.

Expected. UNAUTHORIZED, and the message says the API key was deleted.

  1. Create a second API key in the dashboard and copy it.
  2. Save it in this console and confirm a read works.
  3. Delete that key in the dashboard.
  4. Call GET /api/v1/form again.
  5. The error code is UNAUTHORIZED and the message is “API key was deleted”, not “API key is invalid”.
ManualsafeCustomer API requires Pro

Only meaningful on a community that is not on Pro.

Expected. PLAN_LIMIT, and the message says the Customer API is available on the Pro plan.

  1. Call the API with a key from a community that is not on Pro.
  2. Expect PLAN_LIMIT.
  3. Webhook delivery is also skipped while the community is not on Pro.
ManualwriteIdempotency keys expire after 24 hours

The console cannot wait 24 hours for you.

Expected. The same key creates a new record after 24 hours.

  1. Create an applicant or invitation and copy the Idempotency-Key from the traffic log.
  2. Repeat the same request now and confirm you get the original id.
  3. After 24 hours, send that same key again.
  4. Expect a new record rather than the original.
ManualwriteDo not use the email as the idempotency key

A reused key returns the first person even when the body email changes.

Expected. The scenario “Event and applicant lifecycle” checks this. Run that, or follow the steps.

  1. Create an applicant with a fresh Idempotency-Key.
  2. Send a different email with that same key.
  3. The response id and email belong to the first call.
Manualsafe120 requests a minute, per key

Other clients using the same key share the budget. Use the burst probe on this page.

Expected. RATE_LIMITED, and details.retry_after_seconds says when to retry.

  1. Raise the burst probe only when you can spend the key's minute budget.
  2. A RATE_LIMITED response uses the error envelope.
  3. details.retry_after_seconds is present.
  4. The limit is 120 requests a minute per key, and a community can hold 20 active keys.
ManualsafeForm and event saves do not emit webhooks

Watch Webhooks while you edit a form or an event.

Expected. No delivery for the save. Invitation and applicant changes do emit.

  1. Point the community webhook at the session URL on the Webhooks page.
  2. Edit an event, or load and restore the same join form.
  3. Confirm Webhooks stays quiet for that save.
  4. Create or reject a probe invitation and confirm a delivery arrives.
ManualwriteDashboard and the public page emit the same events

The API is not the only source of webhooks.

Expected. A public join or a dashboard action shows up on Webhooks with a valid signature.

  1. Submit the public join page, or reject someone in the dashboard.
  2. Webhooks shows the same event names the API produces.
  3. The signature verifies, and the read-back matches.
ManualsafeWebhook retries reuse the event id

Force a non-2xx response only long enough to see one retry.

Expected. A later delivery has the same X-Inviter-Event-Id and is marked as a duplicate.

  1. On Webhooks, set forced responses to 500.
  2. Cause one invitation change.
  3. Turn forced responses off immediately. Retries otherwise continue for up to 24 hours.
  4. The next attempt reuses the event id. Order is still not guaranteed; trust updated_at.
ManualwriteCountry rules and reCAPTCHA do not apply to API submit

API submit follows required fields, auto-approval, and email verification only.

Expected. submit succeeds even when the public page would block the visitor.

  1. If the form denies a country or requires reCAPTCHA, submit through this console.
  2. The API should still accept a valid body.
  3. Those checks stay on the public page because the caller is your server.
Manualdeliversend finishes inside the request

Use the playground with mode=send and a mailbox plus a connected platform.

Expected. 201, and the status is sent or delivery_failed. It should not stay pending.

  1. Create an invitation with mode send.
  2. The response status is sent or delivery_failed.
  3. Webhooks shows the delivery lifecycle for that invitation.