---
title: "Best practices"
description: "Server-side only, fail closed, downscale images, and show users messages you choose by category id."
canonical_url: "https://openshield.dev/docs/best-practices"
---
# Best practices

> Server-side only, fail closed, downscale images, and show users messages you choose by category id.

## Call the API from your servers only

A key is a secret. Call openshield from your backend, and give your front end the decision, not the key. The API answers a request that carries a browser `Origin` header with `403 forbidden`, so a call from a browser fails anyway, and a key sent to a browser is exposed.

## Fail closed

A check can fail: a timeout, a network error, a `500` or a `503`. Decide what happens to the content when it does, and make the safe choice the default: **hold it** until a person has looked at it, or until a retry has succeeded. Publishing content that was never checked is the case a moderation service exists to prevent.

- Set a timeout on the call, 10 seconds is a good start.
- Retry `429` and `503` after the `Retry-After` header, at most twice.
- Do not retry `400`, `413` and `415`: the content cannot be accepted as it is. Refuse it.

## Act on the three outcomes

- **accept**: publish.
- **review**: hide the content from other users until a person approves it. If your product has no moderation queue, add the smallest one that fits: a flag on the row and a screen to list the flagged ones.
- **reject**: refuse the content and tell the user why.

## Choose your messages by category id

Each reason has an `id`, such as `nudity` or `pii_email`. It is stable. Map it to messages you write, in your user's language:

```ts
const messages: Record<string, string> = {
  nudity: "This image can't be used here.",
  pii_email: "Please don't share email addresses in comments.",
}
const message = messages[decision.rejectReasons[0]?.id ?? ""] ?? "This content can't be published."
```

The English `text` and `message` of a reason are fallbacks. Do not match on them: they can be reworded.

## Send what you need to send

- **Downscale large photos** to about 1,280 pixels on the longer side: the upload is smaller and faster. Keep the full size of screenshots, where small text or a QR code matters.
- **Send each image once.** Cache the decision with your own record of the content.
- **Check what other people see.** Posts, comments, messages, profile pictures, display names, bios, listings and reviews: anything a user can publish.

## Keep a trail

- Send a `ref`, your own opaque id for the content, so you can find its decision in the [journal](https://openshield.dev/docs/journal-and-ref). Do not put personal data in it.
- Log the `request_id` of every answer next to your own record.

## Keys and workflows

- Give each service its own key, with the narrowest scope and workflows it needs.
- Rotate keys with a grace delay, so a deploy never has a gap.
- Use one workflow per product surface when the rules differ: stricter for a children's section, more lenient for a private chat.

## Test without the network

Stub the HTTP call in your tests, and assert what your code does for `accept`, `review`, `reject` and each kind of error. Do not call the real API from a test suite: it counts against your quota.


## Sitemap

See the full [sitemap](https://openshield.dev/sitemap.md) for all pages.
