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

# Award a custom action

> Record a custom action completion and award points to a customer from your own systems.

Custom actions let you award points for things that happen outside Shopify — a mobile app install, an in-app purchase, an event check-in, a completed survey, or any milestone your own systems track.

You create a custom action once in the Mage admin (**Loyalty Program → Earning rules → Advanced → Custom Action**), give it a stable **action key**, then call this endpoint from your backend whenever the action happens. You can also trigger it with no code using the [Shopify Flow](#trigger-from-shopify-flow) action.

<Note>
  Custom actions are available on the **Growth** plan and above. The customer must already be a loyalty member — a call for someone who is not enrolled returns `404`.
</Note>

Points come from the rule's configured amount, with any applicable VIP tier and bonus campaign multipliers applied automatically. Each rule is either **once per customer** or **unlimited**, depending on how you configured it.

## Request body

| Field             | Type   | Required    | Description                                                                                                                  |
| ----------------- | ------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `action_key`      | string | Yes         | The action key of the custom rule. Lowercase letters, numbers, and underscores only                                          |
| `customer_id`     | string | Conditional | Shopify customer ID (numeric). Required if `customer_email` is not provided                                                  |
| `customer_email`  | string | Conditional | Customer email. Required if `customer_id` is not provided                                                                    |
| `idempotency_key` | string | No          | A unique value for this specific completion. A repeat with the same key returns the original award instead of granting again |
| `metadata`        | object | No          | Free-form context stored alongside the earning record                                                                        |

## Idempotency

If you pass an `idempotency_key`, a repeated request with the same key for the same rule returns the original award with `200 OK` and `replayed: true` — points are never granted twice. For an unlimited rule where each completion should count once, set the key to a stable id for that completion, such as an event id or order id.

<Warning>
  An **unlimited** rule awards on every call. Without an `idempotency_key` there is nothing to deduplicate on, so any retry — a network failure, an app relaunch, a repeated event — awards again and double-grants points. Always send a stable `idempotency_key` per real completion on unlimited rules.

  **Once-per-customer** rules are protected regardless: a second award returns `409`, with or without a key.
</Warning>

## Validations

* Customer must exist and not be excluded from the loyalty program
* A custom action with the given `action_key` must exist and be active
* For a once-per-customer rule, the customer must not have already been awarded this action

## Response

Returns `201 Created` on success, or `200 OK` for an idempotent replay. The `points` and `points_balance` fields are minor currency units on store-credit shops, and the response carries `loyalty_mode` and `currency`. On store-credit shops it also includes `credit_earned` and `credit_balance` companions (major-unit decimal strings), which are absent in points mode. See [Loyalty modes](/api-reference/loyalty-modes).

<Note>
  When the rule has an approval delay configured, `status` is `pending`, `points_given_at` is `null`, `points_due_at` reflects when the points will be approved, and `points_balance` excludes the pending points until then.
</Note>

If the loyalty program is disabled, or test mode is filtering this customer, the request still succeeds with `200 OK` but no points are awarded:

```json theme={null}
{
  "data": null,
  "meta": { "awarded": false, "reason": "program_disabled" }
}
```

## Errors

| Status | Scenario                                                                                              |
| ------ | ----------------------------------------------------------------------------------------------------- |
| `400`  | `action_key` missing or invalid, neither `customer_id` nor `customer_email` provided, or invalid JSON |
| `403`  | Customer is excluded from the loyalty program                                                         |
| `404`  | No active custom action matches the `action_key`, or the customer is not a loyalty member             |
| `409`  | A once-per-customer rule has already been awarded to this customer                                    |
| `429`  | [Rate limit](/api-reference/rate-limits) exceeded (6 requests/second)                                 |

**Example `409` response:**

```json theme={null}
{
  "errors": [
    {
      "status": "409",
      "title": "Conflict",
      "detail": "Customer has already been awarded points for this action. The action is configured to award once per customer."
    }
  ]
}
```

## Trigger from Shopify Flow

If you would rather not write code, add the **Award Custom Action** action to any Shopify Flow workflow and set:

* **Action key** — the same key from the rule's setup page
* **Customer** — map a customer from the workflow's trigger
* **Idempotency key** (optional) — leave blank to use the Flow run id automatically, so a retried run never awards twice

Shopify Flow signs and sends the request for you, so no API key is needed for this path.

<RequestExample>
  ```bash cURL theme={null}
  curl --request POST \
    --url https://api.mageloyalty.com/v1/actions \
    --header 'Authorization: <api-key>' \
    --header 'Content-Type: application/json' \
    --data '{
    "action_key": "mobile_app_install",
    "customer_id": "6789012345",
    "idempotency_key": "install-6789012345-ios"
  }'
  ```

  ```javascript Node theme={null}
  const response = await fetch("https://api.mageloyalty.com/v1/actions", {
    method: "POST",
    headers: {
      Authorization: "<api-key>",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      action_key: "mobile_app_install",
      customer_id: "6789012345",
      idempotency_key: "install-6789012345-ios",
    }),
  });

  const data = await response.json();
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.mageloyalty.com/v1/actions",
      headers={"Authorization": "<api-key>"},
      json={
          "action_key": "mobile_app_install",
          "customer_id": "6789012345",
          "idempotency_key": "install-6789012345-ios",
      },
  )

  data = response.json()
  ```

  ```php PHP theme={null}
  <?php
  $ch = curl_init("https://api.mageloyalty.com/v1/actions");
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => [
          "Authorization: <api-key>",
          "Content-Type: application/json",
      ],
      CURLOPT_POSTFIELDS => json_encode([
          "action_key" => "mobile_app_install",
          "customer_id" => "6789012345",
          "idempotency_key" => "install-6789012345-ios",
      ]),
  ]);
  $data = json_decode(curl_exec($ch), true);
  ```
</RequestExample>

<ResponseExample>
  ```json 201 Awarded theme={null}
  {
    "data": {
      "type": "earning_history_entry",
      "id": "cer_abc123",
      "attributes": {
        "earning_rule_id": "rule_abc",
        "action": "custom",
        "points": 100,
        "points_balance": 1200,
        "loyalty_mode": "points",
        "currency": "GBP",
        "status": "approved",
        "idempotency_key": "install-6789012345-ios",
        "replayed": false,
        "points_given_at": "2026-06-23T12:00:00.000Z",
        "points_due_at": null,
        "created_at": "2026-06-23T12:00:00.000Z"
      }
    }
  }
  ```

  ```json 200 Replay theme={null}
  {
    "data": {
      "type": "earning_history_entry",
      "id": "cer_abc123",
      "attributes": {
        "earning_rule_id": "rule_abc",
        "action": "custom",
        "points": 100,
        "points_balance": 1200,
        "loyalty_mode": "points",
        "currency": "GBP",
        "status": "approved",
        "idempotency_key": "install-6789012345-ios",
        "replayed": true,
        "points_given_at": "2026-06-23T12:00:00.000Z",
        "points_due_at": null,
        "created_at": "2026-06-23T12:00:00.000Z"
      }
    }
  }
  ```

  ```json 409 Conflict theme={null}
  {
    "errors": [
      {
        "status": "409",
        "title": "Conflict",
        "detail": "Customer has already been awarded points for this action. The action is configured to award once per customer."
      }
    ]
  }
  ```
</ResponseExample>
