---
title: "WordPress Firebase Integration: Write to Firestore via REST"
description: "WordPress to Firebase: write posts and orders to Firestore over REST with a self-signed JWT, PATCH upserts, typed values, or an HTTP Cloud Function."
url: "https://wpwebhooks.org/blog/wordpress-firebase-firestore/"
date: "2026-10-28"
---

# WordPress Firebase Integration: Write to Firestore via REST

**TL;DR:** To get WordPress data into Firebase, write it to Cloud Firestore over REST. Sign a service-account JWT, use it directly as the bearer token, and `PATCH` a document whose ID comes from WordPress, so a retry overwrites instead of duplicating.

-   For Firestore, a self-signed JWT with `aud: https://firestore.googleapis.com/` **is** the bearer token. You don't call the token endpoint, and you don't need an SDK.
-   Requests signed by a service account **skip Security Rules**. IAM is the only thing that limits them, and IAM roles cover the whole database.
-   `PATCH` on a document "updates or inserts" it. With the ID set from WordPress, a repeated delivery lands on the same document.
-   Firestore REST needs typed values: `integerValue` is a _string_, maps go in `mapValue.fields`, and arrays cannot hold arrays.
-   The free quota is **20,000 writes a day**. After that, writes cost $0.09 per 100,000 in us-central1.

/ Which Firebase

## What does "WordPress Firebase integration" usually mean?

One of two jobs. The first is **login**: Firebase Authentication as the identity provider for WordPress users. There are plugins for that, and it is a different problem from this article's. The second is **data**: posts, orders, form entries or user profiles copied into Cloud Firestore, where a mobile app or a web front end built on Firebase reads them in real time. This article covers the second job.

The data job usually comes up when a team has a Flutter or React Native app on Firebase and WordPress is still where editors work and customers buy. The app should show the new article, the order status or the member's tier without polling WordPress, and Firestore's realtime listeners do exactly that once the data is in Firestore.

/ Two routes

## Should WordPress write to Firestore directly or through a Cloud Function?

Both work. The difference is where the credential lives and how much it can do.

-   **Directly over REST.** WordPress holds a service-account key and calls the [Firestore REST API](https://firebase.google.com/docs/firestore/use-rest-api). There is nothing else to deploy, and it works on the free Spark plan. The cost is that the key can write anywhere in the database its IAM role covers.
-   **Through an HTTP Cloud Function.** WordPress posts plain JSON to an `onRequest` function with a shared-secret header. The function writes to one collection with the Admin SDK. The key never leaves Google, and the function decides what WordPress is allowed to change. The project has to be on the [Blaze plan](https://firebase.google.com/docs/functions/get-started) to deploy functions.

The security difference is bigger than it looks, and it comes from one sentence in the REST docs: with a service account and an OAuth token, "Cloud Firestore allows these requests to ignore your security rules. Instead, Cloud Firestore uses IAM." The rules you wrote for the app do not apply to WordPress. If the WordPress database or file system leaks the key, the attacker can write to every collection.

A useful rule of thumb: if you are already on Blaze, use the function. If you are not, write directly, and give the service account the narrowest role that works, _Cloud Datastore User_ rather than Owner.

FIG 01 — The WordPress ID becomes the Firestore document ID, so a retried PATCH lands on the same document

/ Auth

## How does WordPress authenticate to Firestore without an SDK?

With a JWT that it signs itself and sends directly as the bearer token. Google's [service account guide](https://developers.google.com/identity/protocols/oauth2/service-account) has an addendum on "Service account authorization without OAuth". For APIs that support it, a signed JWT works in place of an access token, with no request to the authorization server first. Its own example uses `"aud": "https://firestore.googleapis.com/"`.

The claims come straight from the service-account JSON file. `iss` and `sub` are its `client_email`, `kid` is its `private_key_id`, `exp` is "exactly 3600 seconds" after `iat`, and the signature is RS256 with its `private_key`. PHP's `openssl_sign()` produces RS256 with no extra steps:

PHP — self-signed JWT for the Firestore REST API

```
function fs_b64url( string $s ): string {
    return rtrim( strtr( base64_encode( $s ), '+/', '-_' ), '=' );
}

function fs_bearer(): string {
    $cached = get_transient( 'fs_bearer' );
    if ( $cached ) {
        return $cached;
    }
    $sa  = json_decode( file_get_contents( FS_SERVICE_ACCOUNT_FILE ), true );   // outside the web root
    $now = time();
    $jwt = fs_b64url( json_encode( [ 'alg' => 'RS256', 'typ' => 'JWT', 'kid' => $sa['private_key_id'] ] ) )
         . '.' . fs_b64url( json_encode( [
               'iss' => $sa['client_email'],
               'sub' => $sa['client_email'],
               'aud' => 'https://firestore.googleapis.com/',
               'iat' => $now,
               'exp' => $now + 3600,
           ] ) );
    openssl_sign( $jwt, $sig, $sa['private_key'], OPENSSL_ALGO_SHA256 );
    $jwt .= '.' . fs_b64url( $sig );
    set_transient( 'fs_bearer', $jwt, 55 * MINUTE_IN_SECONDS );
    return $jwt;
}
```

The transient keeps the JWT for 55 of its 60 minutes. A retry an hour later signs a new one instead of sending an expired token.

/ Writes

## How do you write a WordPress post or order as a Firestore document?

With `PATCH` to the document's full path, using a document ID you choose. The [patch method](https://firebase.google.com/docs/firestore/reference/rest/v1/projects.databases.documents/patch) "updates or inserts a document". When the document does not exist it is created, and when it does exist it is updated. Using the WordPress ID as the document ID makes each event idempotent: `orders/1234` is always the same document, however many times the delivery is retried.

Firestore REST does not accept plain JSON. Every value is wrapped in an object that names its type, as the [Value reference](https://firebase.google.com/docs/firestore/reference/rest/v1/Value) describes:

PHP — plain PHP values to Firestore typed values, then the PATCH

```
function fs_value( $v ): array {                         // PHP 8.1+ (array_is_list)
    if ( is_null( $v ) )  return [ 'nullValue' => null ];
    if ( is_bool( $v ) )  return [ 'booleanValue' => $v ];
    if ( is_int( $v ) )   return [ 'integerValue' => (string) $v ];   // a string, by spec
    if ( is_float( $v ) ) return [ 'doubleValue' => $v ];
    if ( is_array( $v ) ) {
        return array_is_list( $v )
            ? [ 'arrayValue' => [ 'values' => array_map( 'fs_value', $v ) ] ]
            : [ 'mapValue'   => [ 'fields' => array_map( 'fs_value', $v ) ] ];
    }
    return [ 'stringValue' => (string) $v ];
}

function fs_upsert( string $collection, string $id, array $data ) {
    $url  = 'https://firestore.googleapis.com/v1/projects/' . FS_PROJECT
          . '/databases/(default)/documents/' . $collection . '/' . rawurlencode( $id );
    $mask = implode( '&', array_map( fn( $f ) => 'updateMask.fieldPaths=' . rawurlencode( $f ), array_keys( $data ) ) );

    return wp_remote_request( $url . '?' . $mask, [
        'method'  => 'PATCH',
        'timeout' => 10,
        'headers' => [ 'Authorization' => 'Bearer ' . fs_bearer(), 'Content-Type' => 'application/json' ],
        'body'    => wp_json_encode( [ 'fields' => array_map( 'fs_value', $data ) ] ),
    ] );
}

fs_upsert( 'orders', (string) $order->get_id(), [
    'status'  => $order->get_status(),
    'total'   => (float) $order->get_total(),        // WooCommerce returns a string
    'items'   => count( $order->get_items() ),
    'updated' => time(),
] );
```

Three details in that code:

-   **`updateMask`**. Without it, a PATCH replaces the whole document, so any field the app wrote (say, `seenByCustomer`) is deleted. With the mask, "fields not referenced in the mask … are left unchanged".
-   **Numbers that arrive as strings.** WooCommerce stores prices as strings. Send `"25.00"` and the app cannot sort or sum by total. Cast first.
-   **Arrays inside arrays.** An array value "cannot directly contain another array value". A list of line items, each with its own list of meta, needs each item to be a map.

If you would rather have create-only behaviour, the patch method also takes a precondition, `currentDocument.exists=false`, and [createDocument](https://firebase.google.com/docs/firestore/reference/rest/v1/projects.databases.documents/createDocument) with a `documentId` refuses an ID that already exists. Both turn a retry into an error rather than a no-op. For syncing, the plain upsert is the safer choice.

![Cyberpunk illustration of a vast archive citadel burning with green fire in a stormy night sea, its ring wall of gatehouses sweeping searchlights over a long queue of small boats waiting to be inspected, while a single dark glider slips over the wall above the searchlights and the round vault doors of the towers swing open along its path.](https://wpwebhooks.org/blog/wordpress-firebase-firestore/og_image.jpg)

> A service account doesn't pass through your Security Rules. It goes around them. The rules your app relies on do not protect you from WordPress. — the line in the REST docs that changes the design

| Concern | Hand-rolled call on save\_post | Webhook Actions |
| --- | --- | --- |
| When the call runs | Inside the editor's or customer's request; a slow Google response slows the page | Queued and sent in the background, after the request |
| Firestore unreachable or a 5xx | The change is lost unless you build a retry | Retried with backoff; the document ID makes the repeat land on the same document |
| Typed-value conversion | The fs\_value() function above | Still yours: a pre-dispatch snippet that returns the fields object |
| The bearer JWT | Signed in your code | Signed in your code too, added through the fswa\_headers filter; the log stores Authorization as \[redacted\] |
| Seeing what was sent | error\_log, if you added one | Request and response per attempt in the delivery log, with replay |
| Cloud Function route instead | Same code, different URL | Point the webhook at the function URL with a static secret header from the vault, no JWT at all |

try\_it

Seeing it run beats reading about it. The live preview boots a throwaway WordPress with Webhook Actions already installed and demo deliveries sitting in the log — no signup, nothing left on your machine afterwards.

[Try the live preview →](https://playground.wordpress.net/?blueprint-url=https://wpwebhooks.org/blueprint.json) [Install plugin](https://downloads.wordpress.org/plugin/flowsystems-webhook-actions.zip)

/ Function

## What does the Cloud Function receiver look like?

Small. It checks a shared secret, accepts one shape of document for one collection, and writes it with the Admin SDK. The Admin SDK uses the function's own service account, so WordPress holds only the secret. [HTTP functions](https://firebase.google.com/docs/functions/http-events) can set a timeout of up to an hour, and a single document write needs nowhere near that.

JavaScript — HTTP function that accepts WordPress orders

```
const { onRequest } = require('firebase-functions/v2/https');
const { initializeApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');

initializeApp();

exports.wpOrder = onRequest({ secrets: ['WP_SECRET'] }, async (req, res) => {
  if (req.method !== 'POST' || req.get('X-WP-Secret') !== process.env.WP_SECRET) {
    return res.status(401).end();
  }
  const o = req.body;
  if (!o || !o.id) return res.status(400).end();            // not a retry case

  try {
    await getFirestore().collection('orders').doc(String(o.id)).set({
      status: String(o.status),
      total: Number(o.total),
      updated: Date.now(),
    }, { merge: true });                                    // same upsert semantics as PATCH + mask
  } catch (e) {
    return res.status(500).end();                           // worth a retry
  }
  res.status(200).json({ ok: true });
});
```

The status codes matter as much as the write. A 401 or 400 tells the sender not to try again, because the request itself is wrong. If the write throws, the function returns 500, and a sender that retries on 5xx tries again. The `doc(String(o.id))` makes that retry harmless.

/ Limits

## What are the Firestore limits and costs for WordPress writes?

From the [quotas page](https://firebase.google.com/docs/firestore/quotas): a document can be at most **1 MiB**, an API request at most **10 MiB**, and maps or arrays can nest at most **20** levels deep. A WordPress post with its full HTML content fits easily. A post with every revision does not, so leave revisions out.

A single document has no fixed write rate. The best-practices page says it "depends highly on the workload". The case to avoid is many events updating one shared document, such as a site-wide "latest orders" counter. Write one document per post or per order, and let the app query the collection.

Cost, from the [Firestore pricing](https://cloud.google.com/firestore/pricing) table for us-central1, with each event counting as one write:

-   Free quota: **20,000 writes a day**. A store with 500 orders a day, each updated four times as its status changes, uses 500 × 4 = 2,000 writes, which is 10% of the quota.
-   Beyond the free quota: **$0.09 per 100,000 writes**. One million paid writes a month is 1,000,000 ÷ 100,000 × $0.09 = **$0.90**.

The writes are rarely the expensive part. The reads are: every app user's realtime listener is charged per document it reads, at $0.03 per 100,000 beyond 50,000 a day. A popular app reading the collection you publish will cost more than WordPress writing to it.

/ Exposure

## What does Firebase not protect you from?

**A leaked key with no rules in front of it.** Covered above, and worth repeating: Security Rules do not apply. Store the service-account file outside the web root, never in the database, and rotate it if WordPress is ever compromised.

**Deletes that never arrive.** Trashing a post in WordPress fires its own hook. If you only sync on save, the app keeps showing deleted content. Send a delete, or a `deleted: true` field, from the trash and delete hooks too.

**Changes that arrive out of order.** Two quick saves can be delivered in either order after a retry. Send `updated` as a timestamp and have the app, or the function, ignore anything older than what it has.

The same retried, logged delivery is what keeps the other integrations on this site consistent. [Why WordPress webhooks silently fail](https://wpwebhooks.org/blog/why-wordpress-webhooks-silently-fail-in-production/) explains what goes wrong without it, and [the retry policy article](https://wpwebhooks.org/blog/webhook-retry-policy-exponential-backoff/) covers the schedule.

/Footnotes

¹ REST behaviour, typed values and limits from the [Cloud Firestore documentation](https://firebase.google.com/docs/firestore/use-rest-api); prices from [cloud.google.com/firestore/pricing](https://cloud.google.com/firestore/pricing) (us-central1, default rates), read 2026-11-05.

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"WordPress Firebase Integration: Write to Firestore via REST","description":"WordPress to Firebase: write posts and orders to Firestore over REST with a self-signed JWT, PATCH upserts, typed values, or an HTTP Cloud Function.","datePublished":"2026-10-28","dateModified":"2026-10-28","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/wordpress-firebase-firestore/","image":{"@type":"ImageObject","url":"https://wpwebhooks.org/blog/wordpress-firebase-firestore/og_image.jpg","width":1200,"height":630,"caption":"Cyberpunk illustration of a vast archive citadel burning with green fire in a stormy night sea, its ring wall of gatehouses sweeping searchlights over a long queue of small boats waiting to be inspected, while a single dark glider slips over the wall above the searchlights and the round vault doors of the towers swing open along its path."},"keywords":["wordpress firebase","wordpress firestore","firestore rest api php","wordpress firebase integration","firestore service account jwt","firebase cloud function webhook"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"WordPress Firebase Integration: Write to Firestore via REST","item":"https://wpwebhooks.org/blog/wordpress-firebase-firestore/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"How do I send WordPress data to Firebase?","acceptedAnswer":{"@type":"Answer","text":"Write it to Cloud Firestore, either directly with the Firestore REST API and a service-account credential, or by posting JSON to an HTTP Cloud Function that writes with the Admin SDK. Use the WordPress post or order ID as the document ID so repeated deliveries update the same document."}},{"@type":"Question","name":"Do Firestore Security Rules apply to requests from WordPress?","acceptedAnswer":{"@type":"Answer","text":"Not when WordPress authenticates with a service account. Firestore treats those requests as acting for the application, lets them ignore Security Rules and uses IAM instead. Requests with a Firebase Auth ID token, and unauthenticated ones, are checked against the rules."}},{"@type":"Question","name":"Can I call the Firestore REST API without an access token?","acceptedAnswer":{"@type":"Answer","text":"Yes. Google allows a signed service-account JWT to be used directly as the bearer token for supported APIs, Firestore included. Set aud to https://firestore.googleapis.com/, iss and sub to the service account email, kid to its private key ID, and exp exactly 3600 seconds after iat, signed with RS256."}},{"@type":"Question","name":"Does a Firestore PATCH create the document if it does not exist?","acceptedAnswer":{"@type":"Answer","text":"Yes. The patch method updates or inserts a document. Use updateMask.fieldPaths so fields you do not send are kept, and add the currentDocument.exists precondition if you need update-only or create-only behaviour."}},{"@type":"Question","name":"How much do Firestore writes from WordPress cost?","acceptedAnswer":{"@type":"Answer","text":"The free quota is 20,000 document writes a day. Beyond that, writes in us-central1 cost $0.09 per 100,000, so a million paid writes is $0.90. Reads by app users, at $0.03 per 100,000 beyond 50,000 a day, usually cost more than the writes."}}]}

{"@context":"https://schema.org","@type":"ImageObject","contentUrl":"https://wpwebhooks.org/diagrams/wordpress-firebase-firestore.png","caption":"FIG 01 — The WordPress ID becomes the Firestore document ID, so a retried PATCH lands on the same document","description":"A WordPress event such as an order status change is queued. Route one: WordPress signs an RS256 service-account JWT, uses it directly as the bearer token, and sends PATCH to documents/orders/1234 with an update mask; requests signed by a service account bypass Security Rules. Route two: WordPress posts plain JSON with a shared secret header to an HTTP Cloud Function, which writes the same document with the Admin SDK. In both routes a 5xx is retried and the fixed document id makes the retry overwrite.","encodingFormat":"image/png","creator":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org/"},"copyrightHolder":{"@type":"Organization","name":"Flow Systems","url":"https://flowsystems.pl/"},"copyrightNotice":"© Flow Systems","creditText":"WP Webhooks","license":"https://creativecommons.org/licenses/by/4.0/","acquireLicensePage":"https://wpwebhooks.org/image-license/"}
```
