> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fetchski-attract.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Crop image attachment

> Create a cropped version of an existing image attachment.

```ts AdonisJS wrap theme={"dark"}
import app from '@adonisjs/core/services/app'
import { SiteClient } from '@fetchski/attract-cms-sdk'

const site = await app.container.make(SiteClient)
const result = await site.attachments().cropAttachment({ id: 'ckj0akbxa003vp39kfbxgb8zg', crop: { top: 10, left: 15, width: 300, height: 200 } })
```

```json 200 wrap theme={"dark"}
true
```

## In your app

A controller answers a route on your app: it calls `cropAttachment()` and returns the answer.

**1. Add the controller**

```ts app/controllers/crop_attachment_controller.ts theme={"dark"}
import { inject } from '@adonisjs/core'
import { SiteClient } from '@fetchski/attract-cms-sdk'

@inject()
export default class CropAttachmentController {
  constructor(protected site: SiteClient) {}

  async handle() {
    return this.site.attachments().cropAttachment({ id: 'ckj0akbxa003vp39kfbxgb8zg', crop: { top: 10, left: 15, width: 300, height: 200 } })
  }
}
```

The other values are the example’s: take yours from the request, a form or your database.

**2. Add the route**

```ts start/routes.ts theme={"dark"}
import router from '@adonisjs/core/services/router'

const CropAttachmentController = () => import('#controllers/crop_attachment_controller')

router.post('/attachment/crop', [CropAttachmentController])
```

Put it behind your auth middleware (`.use(middleware.auth())`): it acts with your key’s access.

**3. Call it**

Your own page’s form or script sends `POST /attachment/crop` and gets back its answer as JSON. Test it, below, calls it.

The crop object
is appended to the crops array property of the attachment document.
The newly uploaded image file will be stored with a filename using the
crop properties: \{\_id}-\{name}.\{top}.\{left}.\{width}.\{height}.\{extension}

> [accordion] Required parameters
>
> One object, e.g. `cropAttachment({ id })`; leave out the optional fields.
>
> **id** `string` *required*
>
> **Required.** The \_id property of an existing image attachment document.
>
> **crop** `CropAttachmentRequestCrop` *required*
>
> **Required.** Crop coordinates and dimensions.
>
> **CropAttachmentRequestCrop**
>
> **top** `number` *required*
>
> **Required.** Top coordinate of the crop area (pixels) At least 0.
>
> **left** `number` *required*
>
> **Required.** Left coordinate of the crop area (pixels) At least 0.
>
> **width** `number` *required*
>
> **Required.** Width of the crop area (pixels) At least 1.
>
> **height** `number` *required*
>
> **Required.** Height of the crop area (pixels) At least 1.

> [accordion] Optional parameters
>
> **aposMode** `AposMode | null | undefined`
>
> **Optional.** Leave it out and the site uses `"published"`. `published` (the default) or `draft`. Drafts need a credential that may see them: an API key with the type’s write scope (`content:<type>:write`), or a signed-in editor. The public and a key with only the read scope get 403. One of `draft`, `published`.
>
> **aposLocale** `string | null | undefined`
>
> **Optional.** Leave it out (or pass `null`) and it isn’t sent: the site’s default applies. The locale to read or write, e.g. `en` (default: the site’s default locale).

> [accordion] Returns
>
> A `boolean`. Image cropped successfully.

> [accordion] Errors
>
> Nothing to catch: the SDK retries what is safe, and throws the rest as the classes below. Your exception handler turns them into your app’s own answers, once for every call (see [Map SDK errors to responses](https://docs.fetchski-attract.com/adonisjs/calling-the-api.md#map-sdk-errors-to-responses)); catch only what you want to react to here.
>
> **ValidationFailed** `400`
>
> The site refused what was sent: `errors` lists each field’s problem, `siteMessage` the first.
>
> **Retried first:** No. **Read:** `errors` (field → messages), `siteMessage`.
>
> **AuthenticationFailed** `401`
>
> The API key is missing or refused (malformed, revoked, expired), or a signature its signing mode needs is missing or stale.
>
> **Retried first:** No. **Read:** `siteMessage`, `errorName`, `data`.
>
> **NotFound** `404`
>
> Nothing matches, or the caller may not see it.
>
> **Retried first:** No. **Read:** `siteMessage`, `errorName`, `data`.
>
> **ValidationFailed** `422`
>
> Unprocessable entity - crop coordinates exceed image bounds.
>
> **Retried first:** No. **Read:** `errors` (field → messages), `siteMessage`.
>
> **SiteError** `500`
>
> The site failed. Its logs say why.
>
> **Retried first:** No: a write may have landed. **Read:** `siteMessage`, `errorName`, `data`.
>
> **SiteUnreachable** `no answer`
>
> No answer: DNS, a refused connection, TLS, or the timeout (15 s per attempt by default).
>
> **Retried first:** Only when nothing reached the site (`neverConnected()`); never after a timeout, when it may have landed. **Read:** `reason`, `neverConnected`.
>
> **RequestNotSent** `not sent`
>
> The client refused to send it (e.g. a path segment of `..`); the site never saw it.
>
> **Retried first:** No. **Read:** `reason` (what to change).
>
> **SiteRequestFailed** `any other`
>
> Any other status, or a success whose JSON doesn’t match this SDK version’s types (update the SDK).
>
> **Retried first:** No. **Read:** `reason`, `status`.
>
> **UnknownSite** `setup`
>
> `sites.forSite('acme')` named a slug `config/attract.ts` doesn’t list (with the keys in the database: no row in `attract_api_keys`).
>
> **Retried first:** No: nothing was sent. **Read:** `message`, `slug`.
>
> **SiteConfigurationError** `setup`
>
> The site’s entry is wrong: a malformed `baseUrl` or key, a key filed under another slug, or no default site. `node ace attract:check` names the fix.
>
> **Retried first:** No: nothing was sent. **Read:** `message` (what to fix).

## Behavior

* **Access:** an API key with any one of `content:images:write`, `content:files:write` or `content:videos:write` (or a master above one). A signed-in person may call it as far as their own permissions allow; anyone else gets 403.
* **Rate limit:** the key’s own writes-per-minute limit, set when the key is created (default 120); over it, 429 with `Retry-After`. The client waits and retries for you; you see `RateLimited` only if that runs out.
* **Retries:** a write is sent again only after a 429 (refused before it acted) or when it never reached the site. After a timeout or a 5xx it may have landed: check before you send it again.
* **Timeout:** 15 s per attempt by default; raise `timeout` (milliseconds) in `config/attract.ts` for longer.

## Test it

It calls the controller above with the SDK faked: `fakeAttract()` puts the fake in the container for this one test, and it answers every call with a valid example, so nothing leaves your machine and no key is read. Save it as `tests/unit/attachments_crop_attachment.spec.ts`, then run your tests:

```ts tests/unit/attachments_crop_attachment.spec.ts theme={"dark"}
import { test } from '@japa/runner'
import app from '@adonisjs/core/services/app'
import { fakeAttract } from '@fetchski/attract-cms-adonis/testing'
import CropAttachmentController from '#controllers/crop_attachment_controller'

test('attachments.cropAttachment', async () => {
  const site = fakeAttract()

  const controller = await app.container.make(CropAttachmentController)
  await controller.handle()

  site.assertCalled('attachments.cropAttachment')
})
```

```bash Terminal theme={"dark"}
node ace test
```

## Related

> [card] [Upload media file](https://docs.fetchski-attract.com/adonisjs/reference/attachments/upload-attachment.md)
>
> `site.attachments().uploadAttachment()`

> [card] [POST /@apostrophecms/attachment/crop](https://docs.fetchski-attract.com/api-reference/attachments/crop-image-attachment.md)
>
> The endpoint this calls, in the API reference: the HTTP request and answer, and every rule the site checks.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.