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

# Create a Post

> Publish a post to the GNet network from your own backend — one authenticated call, same result as posting from the Connect portal

## Create a Post API

The **Create Post API** lets your backend publish a post to the GNet network on
behalf of your own company, with a single authenticated call. It produces
exactly the same result as creating the post inside the Connect portal — the
same validation, plan check, rate limits, trust scoring and moderation
routing, in the same order.

There is no multi-step handshake: get a token, call the endpoint.

***

## Endpoint

**POST** `https://dashboard.grdd.net/api/partner/posts`

<Note>
  This endpoint lives on the Connect application host
  (`dashboard.grdd.net`), not on `core.grdd.net` like the other Content API
  endpoints on this page. Use `https://dashboard.grdd.dev` for the development
  environment.
</Note>

***

## Authentication

This endpoint requires a Bearer token issued by GNet core. To obtain one, see
the [Get Token API](/connect-api/get-token).

**Headers:**

```
Authorization: Bearer <access_token>
Content-Type: application/json
Idempotency-Key: <unique string>   (optional, recommended)
```

You may only create posts for **your own company**. The `griddid` in the body
must match the GNET ID your token was issued for (or its parent account);
anything else is rejected with `403 not_entitled`. There is no impersonation
or act-on-behalf-of mode.

***

## Request Body

```json theme={null}
{
  "griddid": "acmelimo",
  "detail": {
    "postType": "update",
    "visibility": "gnet",
    "status": "draft",
    "slug": "pending",
    "title": "New airport shuttle route now live",
    "summary": "Daily LAX ↔ Long Beach service starting August 1.",
    "body": "Full post text here…",
    "imageUrls": ["https://your-cdn.example.com/shuttle.jpg"],
    "ctaUrl": "https://acmelimo.com/shuttle",
    "ctaLabel": "See schedule"
  },
  "logoUrl": "https://your-cdn.example.com/logo.png",
  "countryCode": "US"
}
```

### Top-level fields

| Field         | Type             | Required | Notes                                          |
| ------------- | ---------------- | -------- | ---------------------------------------------- |
| `griddid`     | string           | **Yes**  | Your GNET ID. Must match your token's company. |
| `detail`      | object           | **Yes**  | The post itself — see below.                   |
| `logoUrl`     | string (URL)     | No       | Defaults to your company logo on file.         |
| `countryCode` | string (2 chars) | No       | Defaults to your company's country.            |

### `detail` fields

| Field             | Type            | Required    | Notes                                                                                                          |
| ----------------- | --------------- | ----------- | -------------------------------------------------------------------------------------------------------------- |
| `postType`        | enum            | **Yes**     | See [post types](#post-types).                                                                                 |
| `visibility`      | enum            | **Yes**     | `public` \| `gnet` \| `partners`. May be downgraded — see [visibility](#visibility).                           |
| `status`          | enum            | **Yes**     | Send `"draft"`. **Server-assigned** — see [what the server overrides](#what-the-server-overrides).             |
| `slug`            | string          | **Yes**     | Send any placeholder (e.g. `"pending"`). **Server-generated** from the title.                                  |
| `title`           | string          | **Yes**     | 5–150 characters.                                                                                              |
| `summary`         | string          | No          | Max 300 characters.                                                                                            |
| `body`            | string          | No          | Max 4096 characters.                                                                                           |
| `imageUrls`       | string\[] (URL) | No          | Max 4. Must be already-hosted absolute URLs — see [images](#images).                                           |
| `imageUrl`        | string (URL)    | No          | Single-image legacy field.                                                                                     |
| `ctaUrl`          | string          | No          | Absolute URL, or a site-relative path starting with `/`.                                                       |
| `ctaLabel`        | string          | No          | Max 80 characters.                                                                                             |
| `category`        | string          | No          |                                                                                                                |
| `iconKey`         | string          | No          |                                                                                                                |
| `sourceName`      | string          | No          | Max 120 characters.                                                                                            |
| `sourceLogoUrl`   | string (URL)    | No          |                                                                                                                |
| `eventDate`       | string          | No          |                                                                                                                |
| `eventLocation`   | string          | No          | Max 200 characters.                                                                                            |
| `expiresAt`       | string          | No          |                                                                                                                |
| `pinnedUntil`     | string          | No          |                                                                                                                |
| `isSticky`        | boolean         | No          |                                                                                                                |
| `locales`         | object          | No          | Per-language overrides keyed `en` / `fr` / `de` / `es`, each accepting `title`, `summary`, `body`, `ctaLabel`. |
| `translateOptOut` | boolean         | No          | Skip auto-translation.                                                                                         |
| `typed`           | object          | Conditional | **Required for some post types** — see below.                                                                  |

***

## Post Types

`postType` must be one of:

| Post type            | Rate cap | Default visibility | `typed` required |
| -------------------- | -------- | ------------------ | ---------------- |
| `update`             | 5 / day  | `gnet`             | —                |
| `capacity_available` | 3 / day  | `partners`         | ✅                |
| `press_release`      | 2 / week | `public`           | —                |
| `hiring`             | 2 / week | `public`           | ✅                |
| `fleet_for_sale`     | 3 / week | `public`           | ✅                |
| `event`              | 3 / week | `gnet`             | ✅                |
| `ask_network`        | 5 / week | `gnet`             | ✅                |
| `case_study`         | 1 / week | `public`           | —                |

Exceeding a cap returns `429 rate_limited` with a `Retry-After` header.

### `typed` fields

Five post types require a `detail.typed` object. A missing or invalid `typed`
block fails validation with `422`.

<CodeGroup>
  ```json capacity_available theme={null}
  "typed": {
    "routeFrom": "Los Angeles",     // required, max 120
    "routeTo": "Las Vegas",         // required, max 120
    "startDate": "2026-08-01",      // required
    "endDate": "2026-08-14",        // optional
    "vehicleType": "SPRINTER"       // optional, max 80
  }
  ```

  ```json hiring theme={null}
  "typed": {
    "role": "Chauffeur — Nights",           // required, max 120
    "location": "Palm Beach, FL",           // optional, max 120
    "applyUrl": "https://acmelimo.com/jobs" // optional, must be a URL
  }
  ```

  ```json fleet_for_sale theme={null}
  "typed": {
    "vehicleRef": "2021 Mercedes Sprinter", // required, max 120
    "year": 2021,                           // optional, integer 1950–2100
    "price": "$68,000",                     // optional, max 40
    "location": "Palm Beach, FL"            // optional, max 120
  }
  ```

  ```json event theme={null}
  "typed": {
    "eventDate": "2026-09-12",                       // required
    "eventLocation": "Las Vegas Convention Center",  // optional, max 200
    "registrationUrl": "https://example.com/reg"     // optional, must be a URL
  }
  ```

  ```json ask_network theme={null}
  "typed": {
    "question": "Anyone running SUVs out of SNA on weekends?", // required, max 500
    "topicTags": ["airport", "suv"]                            // optional, max 8 tags
  }
  ```
</CodeGroup>

***

## What the Server Overrides

Three fields are required by the schema but **decided by the server** — send
placeholders and read the real values back from the response:

* **`status`** — always recomputed. Companies flagged as post-trusted publish
  immediately (`published`); everyone else is queued for moderator review
  (`pending_review`). You cannot self-publish by sending `"status": "published"`.
* **`slug`** — always regenerated from your `title`, scoped to your GNET ID.
* **`authorTrustAtPublish`** — computed from your profile score, partner count,
  plan tier and account age.

### Visibility

If you request `"visibility": "public"` but your trust score is below the
public-visibility floor, the post is accepted and downgraded to `gnet`. When
this happens the response includes `"visibilityDowngraded": true`.

### Images

There is no partner image-upload endpoint. Host your images yourself and pass
absolute URLs in `imageUrls` (maximum 4).

***

## Request Example

```bash theme={null}
curl -X POST \
  'https://dashboard.grdd.net/api/partner/posts' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 8f1c2e64-1f2a-4c8e-9d3b-6a7f5e1c2d90' \
  -d '{
    "griddid": "acmelimo",
    "detail": {
      "postType": "update",
      "visibility": "gnet",
      "status": "draft",
      "slug": "pending",
      "title": "New airport shuttle route now live",
      "summary": "Daily LAX to Long Beach service starting August 1.",
      "body": "We are adding two daily runs…"
    }
  }'
```

***

## Response

### 201 Created — published immediately

```json theme={null}
{
  "ok": true,
  "id": "0f3b8c6e-8b41-4f0a-9a2f-1d6c5b4e7a90",
  "slug": "new-airport-shuttle-route-now-live-acmelimo",
  "status": "published",
  "visibilityDowngraded": false
}
```

### 202 Accepted — queued for review

```json theme={null}
{
  "ok": true,
  "id": "0f3b8c6e-8b41-4f0a-9a2f-1d6c5b4e7a90",
  "slug": "new-airport-shuttle-route-now-live-acmelimo",
  "status": "pending_review",
  "visibilityDowngraded": false
}
```

<Warning>
  `202` means the post was created but is **not visible yet** — a GNet moderator
  must approve it. There is no status-polling endpoint today, and no callback is
  sent on approval.
</Warning>

Every response carries an `ok` boolean. Never treat a 2xx-shaped body as
success without checking it.

***

## Error Responses

| Status | `error`                | Meaning                                                                                                             |
| ------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `400`  | `bad_request`          | Body is not valid JSON, or not a JSON object.                                                                       |
| `401`  | `missing_token`        | No `Authorization: Bearer` header.                                                                                  |
| `401`  | `invalid_token`        | Token rejected by core.                                                                                             |
| `401`  | `expired_token`        | Token expired — request a new one.                                                                                  |
| `403`  | `not_entitled`         | `griddid` is not your token's company.                                                                              |
| `403`  | `plan_required`        | Your Connect plan or trial does not allow posting.                                                                  |
| `404`  | `operator_not_found`   | The GNET ID could not be resolved.                                                                                  |
| `409`  | `idempotency_conflict` | Same `Idempotency-Key` reused with a different body.                                                                |
| `422`  | `validation_error`     | Payload failed schema validation; `details` carries the field-level errors.                                         |
| `429`  | `rate_limited`         | Rate cap for this post type reached; `details.resetsAt` is an epoch-ms timestamp and a `Retry-After` header is set. |
| `500`  | `internal_error`       | Unexpected server error.                                                                                            |
| `502`  | `core_unreachable`     | Token verification upstream failed — retry later.                                                                   |

```json theme={null}
{
  "ok": false,
  "error": "validation_error",
  "details": {
    "formErrors": [],
    "fieldErrors": {
      "detail.title": ["String must contain at least 5 character(s)"]
    }
  }
}
```

***

## Retrying Safely

Pass an `Idempotency-Key` header — any unique string per logical post, such as
a UUID you generate before the first attempt.

* **Same key, same body** → replays the original response instead of creating
  a second post.
* **Same key, different body** → `409 idempotency_conflict`.
* Keys are scoped to your caller identity and expire after **24 hours**.

Without an idempotency key, a network timeout followed by a retry can create
duplicate posts.

***

## Usage Examples

### JavaScript/Node.js

```javascript theme={null}
const response = await fetch(
  "https://dashboard.grdd.net/api/partner/posts",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({
      griddid: "acmelimo",
      detail: {
        postType: "update",
        visibility: "gnet",
        status: "draft",
        slug: "pending",
        title: "New airport shuttle route now live",
        summary: "Daily LAX to Long Beach service starting August 1.",
      },
    }),
  },
);

const result = await response.json();
if (!result.ok) {
  throw new Error(`${response.status} ${result.error}`);
}
console.log(result.status === "published" ? "Live" : "Awaiting review");
console.log("Post ID:", result.id, "slug:", result.slug);
```

### Python

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

url = "https://dashboard.grdd.net/api/partner/posts"
headers = {
    "Authorization": f"Bearer {access_token}",
    "Content-Type": "application/json",
    "Idempotency-Key": str(uuid.uuid4()),
}
payload = {
    "griddid": "acmelimo",
    "detail": {
        "postType": "update",
        "visibility": "gnet",
        "status": "draft",
        "slug": "pending",
        "title": "New airport shuttle route now live",
        "summary": "Daily LAX to Long Beach service starting August 1.",
    },
}

response = requests.post(url, json=payload, headers=headers)
result = response.json()

if not result.get("ok"):
    raise RuntimeError(f"{response.status_code} {result.get('error')}")

print("Post ID:", result["id"])
print("Status:", result["status"])
```
