Context
IgSender already implements the Instagram Graph API two-step flow (container creation + media publish), but the private helper UploadImageToPublicUrl always throws NotImplementedException. This is the only blocker preventing Instagram from going live.
Additionally, the two Instagram time-slots in GeneratorFactory (10:00 and 18:00) are commented out and must be re-enabled once the sender is stable.
This issue covers everything required to bring Instagram to production.
Part 1 — Instagram Platform Prerequisites
Before writing any code, complete the following steps on the Instagram / Meta Developer side.
1.1 Meta Developer App
- Go to https://developers.facebook.com/apps and create a new app (type: Business).
- Add the Instagram Graph API product to the app.
- In App Roles, add your Instagram business account as a tester or assign it directly.
1.2 Instagram Business Account
The Graph API only works with Instagram Business or Creator accounts:
- Open the Instagram mobile app → Settings → Account → Switch to Professional Account.
- Choose Business.
- Connect the Instagram account to a Facebook Page (required for Graph API access).
1.3 Generate a Long-Lived Access Token
- In Meta for Developers, go to Tools → Graph API Explorer.
- Select your app and generate a User Token with scopes:
instagram_basic
instagram_content_publish
pages_read_engagement
- Exchange the short-lived token for a long-lived token (valid 60 days):
GET https://graph.facebook.com/v20.0/oauth/access_token
?grant_type=fb_exchange_token
&client_id={APP_ID}
&client_secret={APP_SECRET}
&fb_exchange_token={SHORT_LIVED_TOKEN}
- Store the resulting token in
IG_ACCESS_TOKEN.
⚠️ Tokens expire after 60 days. Manual rotation is required until a refresh flow is implemented.
1.4 Find Your Instagram Account ID
GET https://graph.facebook.com/v20.0/me/accounts
?access_token={IG_ACCESS_TOKEN}
Find the connected Facebook page, then:
GET https://graph.facebook.com/v20.0/{PAGE_ID}?fields=instagram_business_account&access_token={IG_ACCESS_TOKEN}
The instagram_business_account.id value is your IG_ACCOUNT_ID.
Part 2 — Azure Blob Storage Setup (Public Image Hosting)
The Instagram Graph API requires a publicly accessible image URL to create a media container. The simplest solution within the existing Azure infrastructure is Azure Blob Storage with anonymous read access.
2.1 Azure Setup
- Create (or reuse) an Azure Storage Account in the same resource group as the Azure Function.
- Create a Blob Container named
xposter-images with Blob (anonymous read) access level.
- In the Function App settings, add:
| Variable |
Description |
AZURE_STORAGE_CONNECTION_STRING |
Connection string for the Storage Account |
AZURE_STORAGE_CONTAINER_NAME |
Container name (e.g. xposter-images) |
- For production, consider using Managed Identity instead of a connection string:
- Assign
Storage Blob Data Contributor role to the Function App's managed identity.
- Use
DefaultAzureCredential with a BlobServiceClient.
2.2 Blob Lifecycle Management
Images are ephemeral (only needed during the Instagram API call). Add a lifecycle rule to delete blobs older than 1 day:
{
"rules": [{
"name": "delete-old-images",
"type": "Lifecycle",
"definition": {
"actions": { "baseBlob": { "delete": { "daysAfterModificationGreaterThan": 1 } } },
"filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["xposter-images/"] }
}
}]
}
Part 3 — Code Implementation
3.1 Add Azure.Storage.Blobs NuGet package
dotnet add src/XPoster.csproj package Azure.Storage.Blobs
3.2 Implement UploadImageToPublicUrl in IgSender.cs
Replace the NotImplementedException stub:
private async Task<string> UploadImageToPublicUrl(byte[] image)
{
var connectionString = Environment.GetEnvironmentVariable("AZURE_STORAGE_CONNECTION_STRING")
?? throw new InvalidOperationException("AZURE_STORAGE_CONNECTION_STRING is not set.");
var containerName = Environment.GetEnvironmentVariable("AZURE_STORAGE_CONTAINER_NAME")
?? "xposter-images";
var blobName = $"{Guid.NewGuid()}.jpg";
var blobServiceClient = new BlobServiceClient(connectionString);
var containerClient = blobServiceClient.GetBlobContainerClient(containerName);
await containerClient.CreateIfNotExistsAsync(PublicAccessType.Blob);
var blobClient = containerClient.GetBlobClient(blobName);
using var stream = new MemoryStream(image);
await blobClient.UploadAsync(stream, new BlobHttpHeaders { ContentType = "image/jpeg" });
_logger.LogInformation("Image uploaded to blob: {BlobUri}", blobClient.Uri.ToString());
return blobClient.Uri.ToString();
}
3.3 Inject dependencies via constructor
To make IgSender testable, inject IBlobStorageService (a new interface wrapping the Blob client) rather than instantiating it directly. This follows the same pattern used by InSender and XSender.
Interface:
public interface IBlobStorageService
{
Task<string> UploadAsync(byte[] data, string contentType);
}
Update IgSender constructor to accept IBlobStorageService blobStorageService.
Register in Program.cs:
builder.Services.AddTransient<IBlobStorageService, AzureBlobStorageService>();
builder.Services.AddTransient<IgSender>();
3.4 Enable Instagram slots in GeneratorFactory
Once the sender is verified in staging, uncomment:
{ 10, MessageSender.IgSummaryFeed },
{ 18, MessageSender.IgPowerLow },
Part 4 — Environment Variables Summary
Update src/local.settings.json.example and docs/configuration.md:
| Variable |
Required |
Description |
IG_ACCESS_TOKEN |
✅ Yes |
Long-lived Instagram Graph API token |
IG_ACCOUNT_ID |
✅ Yes |
Instagram Business Account numeric ID |
AZURE_STORAGE_CONNECTION_STRING |
✅ Yes |
Azure Storage connection string for image hosting |
AZURE_STORAGE_CONTAINER_NAME |
Optional |
Blob container name (default: xposter-images) |
Part 5 — Tests to Add
tests/SenderPlugins/IgSenderTests.cs:
SendAsync_WhenImageIsNull_LogsWarningAndReturnsFalse
SendAsync_WhenBlobUploadSucceeds_CreatesMediaContainerWithCorrectUrl
SendAsync_WhenMediaContainerFails_ReturnsFalse
SendAsync_WhenPublishFails_ReturnsFalse
SendAsync_WhenCaptionExceedsLimit_TruncatesCaption
Acceptance Criteria
Related
Context
IgSenderalready implements the Instagram Graph API two-step flow (container creation + media publish), but the private helperUploadImageToPublicUrlalways throwsNotImplementedException. This is the only blocker preventing Instagram from going live.Additionally, the two Instagram time-slots in
GeneratorFactory(10:00 and 18:00) are commented out and must be re-enabled once the sender is stable.This issue covers everything required to bring Instagram to production.
Part 1 — Instagram Platform Prerequisites
Before writing any code, complete the following steps on the Instagram / Meta Developer side.
1.1 Meta Developer App
1.2 Instagram Business Account
The Graph API only works with Instagram Business or Creator accounts:
1.3 Generate a Long-Lived Access Token
instagram_basicinstagram_content_publishpages_read_engagementIG_ACCESS_TOKEN.1.4 Find Your Instagram Account ID
Find the connected Facebook page, then:
The
instagram_business_account.idvalue is yourIG_ACCOUNT_ID.Part 2 — Azure Blob Storage Setup (Public Image Hosting)
The Instagram Graph API requires a publicly accessible image URL to create a media container. The simplest solution within the existing Azure infrastructure is Azure Blob Storage with anonymous read access.
2.1 Azure Setup
xposter-imageswith Blob (anonymous read) access level.AZURE_STORAGE_CONNECTION_STRINGAZURE_STORAGE_CONTAINER_NAMExposter-images)Storage Blob Data Contributorrole to the Function App's managed identity.DefaultAzureCredentialwith aBlobServiceClient.2.2 Blob Lifecycle Management
Images are ephemeral (only needed during the Instagram API call). Add a lifecycle rule to delete blobs older than 1 day:
{ "rules": [{ "name": "delete-old-images", "type": "Lifecycle", "definition": { "actions": { "baseBlob": { "delete": { "daysAfterModificationGreaterThan": 1 } } }, "filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["xposter-images/"] } } }] }Part 3 — Code Implementation
3.1 Add
Azure.Storage.BlobsNuGet package3.2 Implement
UploadImageToPublicUrlinIgSender.csReplace the
NotImplementedExceptionstub:3.3 Inject dependencies via constructor
To make
IgSendertestable, injectIBlobStorageService(a new interface wrapping the Blob client) rather than instantiating it directly. This follows the same pattern used byInSenderandXSender.Interface:
Update
IgSenderconstructor to acceptIBlobStorageService blobStorageService.Register in
Program.cs:3.4 Enable Instagram slots in
GeneratorFactoryOnce the sender is verified in staging, uncomment:
Part 4 — Environment Variables Summary
Update
src/local.settings.json.exampleanddocs/configuration.md:IG_ACCESS_TOKENIG_ACCOUNT_IDAZURE_STORAGE_CONNECTION_STRINGAZURE_STORAGE_CONTAINER_NAMExposter-images)Part 5 — Tests to Add
tests/SenderPlugins/IgSenderTests.cs:SendAsync_WhenImageIsNull_LogsWarningAndReturnsFalseSendAsync_WhenBlobUploadSucceeds_CreatesMediaContainerWithCorrectUrlSendAsync_WhenMediaContainerFails_ReturnsFalseSendAsync_WhenPublishFails_ReturnsFalseSendAsync_WhenCaptionExceedsLimit_TruncatesCaptionAcceptance Criteria
UploadImageToPublicUrluploads to Azure Blob and returns a valid HTTPS URLIgSender.SendAsyncsuccessfully publishes a post with image to Instagram (tested in staging)NotImplementedExceptionis thrown at runtimeAZURE_STORAGE_*variables documented indocs/configuration.mdandlocal.settings.json.exampleGeneratorFactoryIBlobStorageServicein tests)Related
NotImplementedExceptioninIgSender.UploadImageToPublicUrl